TDD三定律
1、在编写不能通过的单元测试前,不可编写生产代码。
2、只可编写刚好无法通过的单元测试,不能编译也算不通过。
3、只可编写刚好足以通过当前失败测试的生产代码。
TDD的关键步骤
- 添加一个小的测试
- 运行所有测试并且失败
- 做一点修改
- 运行所有测试并且成功
- 重构以消除重复
基于这个思想,上codewars找一道题实践一把tdd。
ATM machines allow 4 or 6 digit PIN codes and PIN codes cannot contain anything but exactly 4 digits or exactly 6 digits.
If the function is passed a valid PIN string, return true, else return false.
eg:
Solution.validatePin("1234") === true
Solution.validatePin("12345") === false
Solution.validatePin("a234") === false
根据题目来看,写一个方法,输入是一个密码,返回一个boolean值。根据tdd关键步骤,先添加一个测试。
codewars已经有示例了。
@Test
public void validPins() {
assertEquals(true, Solution.validatePin("1234"));
assertEquals(true, Solution.validatePin("0000"));
assertEquals(true, Solution.validatePin("1111"));
assertEquals(true, Solution.validatePin("123456"));
assertEquals(true, Solution.validatePin("098765"));
assertEquals(true, Solution.validatePin("000000"));
assertEquals(true, Solution.validatePin("090909"));
}
接下来到第二步,运行所有测试并失败。很显然会失败,因为Solution就没有这个类,这里不运行,先创建solution类。(第三步做一点修改)
public class Solution {
}
good news Solution不报错了。但是validatePin报错了,因为还没有这个方法。运行测试,失败了。哦哦,来继续做一点修改以通过测试。添加validatePin方法。
public static boolean validatePin(String pin){
return true;
}
ok不报错了。
我们看第一个测试方法validPins,全部验证的是true。那我们直接返回true,运行测试。
测试成功了。到此结束了吗?好像没有,我们的测试用例没有覆盖到需求。还需要写更多的测试用例以满足需求。看看需求:四位或六位的数字。添加测试:
@Test
public void nonDigitCharacters() {
assertEquals(false, Solution.validatePin("a234"));
assertEquals(false, Solution.validatePin(".234"));
}
@Test
public void invalidLengths() {
assertEquals(false, Solution.validatePin("1"));
assertEquals(false, Solution.validatePin("12"));
assertEquals(false, Solution.validatePin("123"));
assertEquals(false, Solution.validatePin("12345"));
assertEquals(false, Solution.validatePin("1234567"));
assertEquals(false, Solution.validatePin("-1234"));
assertEquals(false, Solution.validatePin("1.234"));
assertEquals(false, Solution.validatePin("00000000"));
}
ok,这里添加了2个测试方法,一个是测试非数字的,一个是测试位数的。重复tdd步骤,运行测试。
2个失败了,1个通过了。做一点小修改,以通过测试用例。当输入“a234”的时候或者“.234”的时候,返回false。修改方法validatePin
public static boolean validatePin(String pin){
if("a234".equals(pin) || ".234".equals(pin)){
return false;
}
return true;
}
这里因为练习pdd的步骤,所以每一步严格按照tdd的步骤走了,所以看起来这里的代码比较蠢。
ok运行测试
image.png
棒棒的,我们严格满足了定律3,只编写刚好通过测试用例的代码。ok,暂时搞定2个测试用例了,我们进行下一步。位数测试,修改一点点代码满足测试3
public static boolean validatePin(String pin){
if("a234".equals(pin) || ".234".equals(pin)){
return false;
}
if(pin.length()!=4 && pin.length()!=6){
return false;
}
return true;
}
运行测试
全通过了,进行下一步,重构以消除重复。
现在的方法看起来是这样
public static boolean validatePin(String pin){
if("a234".equals(pin) || ".234".equals(pin)){
return false;
}
if(pin.length()!=4 && pin.length()!=6){
return false;
}
return true;
}
看到问题了,里面有硬编码,所以我们要消除硬编码。这段硬编码我们是为了满足测试2,测试2是为了测试是不是纯数字,做一点小修改
public static boolean validatePin(String pin){
Pattern pattern = Pattern.compile("^-?\\d+(\\.\\d+)?$");
Matcher isNum = pattern.matcher(pin);
if (!isNum.matches()) {
return false;
}
if(pin.length()!=4 && pin.length()!=6){
return false;
}
return true;
}
运行测试,ok还是通过了。
下一步,重构以消除重复。这个方法细分来看,做了2件事,第一是判断是否包含数字之外的字符,第二是判断位数,那么把这两个事可以单独提出来提炼一个方法。重构如下
public class Solution {
public static boolean validatePin(String pin){
return validateNonDigitCharacters(pin) && invalidLengths(pin);
}
private static boolean validateNonDigitCharacters(String pin){
Pattern pattern = Pattern.compile("^-?\\d+(\\.\\d+)?$");
Matcher isNum = pattern.matcher(pin);
return isNum.matches();
}
private static boolean invalidLengths(String pin){
return !(pin.length()!=4 && pin.length()!=6);
}
}
invalidLengths方法似乎还有些不是很能一眼看懂,不过仔细看看还是能懂,guess what,我不改了。
运行测试
测试通过了,到此结束。
总结
我把tdd理解为 逻辑代码未动,测试代码先行。通过不停的满足覆盖全面的测试代码,一点点修改逻辑代码,直到满足所有测试。
TODO
可以在原需求基础增加一个需求,继续迭代开发,体现出tdd的优势
bug
测试用例没有覆盖完全,没有考虑空值
最优解
public static boolean validatePin(String pin) {
return pin.matches("\\d{4}|\\d{6}");
}
网友评论