TDD是敏捷开发极限编程的很有意思的一种技术,因为不符合开发常规思维而有意思。究竟要不要测试驱动开发,能否驱动,在怎样的范围内可采用,是否切实可行?其实这个技术的确已经有许多先驱者使用并且已经品尝到了好处。关于TDD的好处,我放在另一篇来介绍。
其实许多技术都是被人为的神化,接近它就发现并没有那么可怕。这次让我们玩起来,边玩边学TDD。学习之前,先要会玩游戏,玩玩看,你能过几关。
可以先Play Hangman点这里玩游戏,hangman就是上吊的人。这是一个古老的简单的猜单词的游戏。
具体内容,可以点这里看百度的解释。
(这里是以JUnit和Java为框架来学习)
任务:我们要用TDD的方法来实现一个小游戏的核心代码。并不关注界面的实现。
TDD(测试驱动开发)是基于黑盒的讨论,甚至可以思考为是一个基于需求的测试,这就使得这种测试技术带有了无穷的魅力!下面让我们一起来揭开它神秘的面纱。
先来看看需求吧,游戏界面一共有两个,一个是主界面,一个是solution界面。solution用来显示游戏结果。主界面是核心。
主界面里用红框已经标示出这段程序的重要核心部分。现在来整理需求:
New:启动游戏。启动游戏完成Tries初始化为12,Length初始化为单词长度,Used初始化为“AEIOU”
Play:玩游戏,其实就是玩着尝试字母,会有两种情况:
1)输入正确
used增加一个字母;单词上增加相应的字母。
2)输入错误
used中没有的,就used增加该字母;Tries减1;
Result:
Game Win, 猜的字和原单词相同
Game Over, Tries<0
TDD的过程:写个会出错的测试(红色)->编写代码(绿色)-->代码重构(蓝色)简洁代码。循环的过程。
接下来,我们针对需求从测试代码开始。
让我们来感受,从测试入手,从需求入手。
我们先来完成第一个需求:
启动游戏
启动游戏的需求是:Tries初始化为12,Length初始化为单词长度,Used初始化为“AEIOU”
先完成测试代码:
我们假定第一个试猜的是 "HELLO"
public class startGame {
@Test
public void testLength() {
HangMan hangMan=new HangMan("HELLO");
assertEquals(5,hangMan.length());
}
@Test
public void testTries() {
HangMan hangMan=new HangMan("HELLO");
assertEquals(12,hangMan.tries());
}
@Test
public void testUsed() {
HangMan hangMan=new HangMan("HELLO");
assertEquals("AEIOU",hangMan.used());
}
}
写的过程,你可能就开始抱怨,hangMan根本没有嘛。没关系,先写,然后让IDE自己来查错,生成,少了我们写的步骤,那些方法也让它自动生成!
跑测试!全部红色!OK,这正是我们需要的,先要编写找到缺陷的测试!很简单!
TDD,要满足小步快跑,从简单入手,每次只完成一个任务。
第二步就是编码!
所以在HangMan这个类中,你只需写:
public int length() {
return 5;
}
public int tries() {
return 12;
}
public String used() {
return “AEIOU”;
}
然后跑测试!全部绿色!快速让测试通过!哇塞,你会惊呼这个什么意思?是编程吗?硬通过?!
是的,我们的原则就是让测试快速通过!当然随着测试的深入,硬通过是不行的,需要加些技术和算法,我们将一步步的深入,这样让问题简单,而且每一步都走的有信心!而且每次的测试都能回归!
第三步,重构代码和测试脚本!
这里代码没什么好重构的。测试脚本重复的语句,整理一下。
public class startGame {
HangMan hangMan=new HangMan("HELLO");
@Test
public void testLength() {
assertEquals(5,hangMan.length());
}
@Test
public void testTries() {
assertEquals(12,hangMan.tries());
}
@Test
public void testUsed() {
assertEquals("AEIOU",hangMan.used());
}
}
再跑测试!绿色!OK,重构后没有引入任何问题!那么可以提交第一部分的代码啦!
接下来我们考虑,如果把单词换一个!
再执行测试,测试出错了!长度的测试出错
OK,这正是我们要的,第一步,让测试出错。
第二步,我们来编写代码:
public HangMan(String word) {
this.word=word;
}
public int length() {
return this.word.length();
}
}
再执行测试!绿色!OK,通过测试。
进行重构代码。
到此为止:启动游戏的工作就做完啦!
接下来是,需求的第二部分:Play
Play:玩游戏,其实就是玩着尝试字母,会有两种情况:
1)输入正确used增加一个字母;单词上增加相应的字母。
2)输入错误
used中没有的,就used增加该字母;Tries减1;
写测试代码,(小步快跑是可以从一个测试方法入手,一个一个的TDD,这里限于篇幅,全部写出来)我们根据需求写出所有的测试。
public class playGame {
HangMan hangMan=new HangMan("HELLO");
@Test
public void input_Correct_VowelChar() {
hangMan.types('O');
assertEquals("AEIOU",hangMan.used());
}
@Test
public void input_Correct_ConsonantChar() {
hangMan.types('L');
assertEquals("AEIOUL",hangMan.used());
}
@Test
public void input_Wrong_Char() {
hangMan.types('M');
assertEquals(11,hangMan.tries());
assertEquals("AEIOUM",hangMan.used());
}
}
跑测试!红色!OK,正是我们需要的!
第二步,编码,使得测试能快速通过。
第三步:重构,整理代码,Clean it!
以此类推,我们写出最后一部分
游戏结果(Game Result)的测试脚本。
public class gameResult {
HangMan hangMan=new HangMan("HELLO");
@Test
public void gameWin() {//输入有效的字母,完成猜测
hangMan.types('H');
hangMan.types('L');
assertEquals("HELLO",hangMan.solutionIni());
}
@Test
public void gameLose() {//输入的次数超出12次,游戏失败!
for(int i=0;i<12;i++){
hangMan.types('K');
}
assertEquals(0,hangMan.tries());
}
}
跑测试!红色!OK
编写代码!跑测试!绿色!OK
代码重构!跑测试!绿色!OK!
最后代码整理结果如下,代码重构,使得方法的代码尽量短,简单简洁!以下代码还可以再不断完善出不同版本和风格的代码!
public class HangMan {
String usedString="AEIOU";
char typeCh;
String word="";
String solutionWord="";
int triesNumber=12;
public HangMan(String word) {
this.word=word;
generationSolution();
}
private void generationSolution() {
for(int i=0;i<word.length();i++){
solutionWord+="_";
}
}
public int length() {
return this.word.length();
}
public int tries() {
return triesNumber;
}
public String used() {
return usedString;
}
public void types(char ch) {
char[] solutionArray = replaceChar(ch);
triesReduce(ch);
usedIncrease(ch);
solutionWord=String.valueOf(solutionArray);
}
private void usedIncrease(char ch) {
if(usedString.indexOf(ch)<0&&ch!='\0'){
usedString+=ch;
};
}
private void triesReduce(char ch) {
if(word.indexOf(ch)<0&&ch!='\0'){
triesNumber--;
}
}
private char[] replaceChar(char ch) {
char[] wordArray=word.toCharArray();
char[] solutionArray=solutionWord.toCharArray();
for(int i=0;i<word.length();i++){
if(ch==wordArray[i]){
solutionArray[i]=wordArray[i];
}
}
return solutionArray;
}
public String solutionIni() {
char[] wordArray=word.toCharArray();
char[] solutionArray=solutionWord.toCharArray();
for(int i=0;i<word.length();i++){
if("AEIOU".indexOf(wordArray[i])>=0){
solutionArray[i]=wordArray[i];
}
}
solutionWord=String.valueOf(solutionArray);
return solutionWord;
}
}
至此,我们领略了好玩的hangMan,也领略了别有风味的TDD。在TDD中更多感受的是实现需求,实现需求,实现需求,重构代码,重构代码,重构代码。
这种重需求,重实现,轻算法的开发思路,孰是孰非,各位看官自行评断咯。