个人实践的TDD备忘录。
1. 什么是TDD
测试驱动开发,可以粗糙的理解为先写单元测试,再写代码。 TDD的思路很简单,当代码执行的每一步都是可信的(可以按照预期正确运行), 那么最终获得的结果也必定是正确的。那么通过单元测试来保证其中每一步的可信,也就可以保证最终运行的结果。
2. 实现
red-green-refactor cycle
设想一个简单的需求:读取周边wifi热点的状态,输出为json串,如果读不到就输出空json
step 1: 阅读需求,设计暴露的接口
private List<ScanResult> loadScanResult(Context c) //获取Wi-Fi列表
private List<WifiMacInfo> scanResultToWifiMacInfo(List<ScanResult> ls); //过滤并生成需求的mac列表。
step 2: 构建单元测试
举一个接口的例子
@Test
public void testScanResultToListSizeShouldBeZero(){
List<WifiMacInfo> res = FingerprintManager.scanResultToWifiMacInfo(null);
assertEquals(res.size(), 0);
List<ScanResult> ls = new ArrayList<ScanResult>();
res = FingerprintManager.scanResultToWifiMacInfo(ls);
assertEquals(res.size(), 0);
}
step3: 实现空白接口,使全部单测失效
private List<WifiMacInfo> scanResultToWifiMacInfo(List<ScanResult> ls){
return null;
}
step4: 修改接口实现,使测试通过
这时候才是真正的编码环节
private List<WifiMacInfo> scanResultToWifiMacInfo(List<ScanResult> ls){
return new ArrayList<WIfiMacInfo>(); //假装实现
}
step5: 重构运行全部单测使其通过
重构通常不需要动单测嗲吗,只要修改逻辑代码然后运行测试代码全部通过即能保证逻辑不变。
3. 使用场景
-
业务逻辑相对稳定: 业务逻辑变化越大,可复用的粒度就会越细。若业务逻辑改动到结构框架都有变化,那么TDD中单测代码就只剩下构建当次的的使用价值,而且还要考虑到单测代码本身也可能是有bug的,尤其是在首次使用。
-
连续重构,如代码性能优化。TDD预先构建的测试程序,能有效保障业务逻辑前后的一致性。
-
逻辑多,runtime(如网络,UI等难以预估的模块)相关性少 不可控的部分均需要mock,会大大降低测试代码的兼容性。
4. 代码的可测性
4.1. 尽量构建为没有副作用(side effects)/状态的纯函数
所谓纯函数即调用时固定的输入产生固定的输出。
当一个方法调用后,除了方法返回值,还对其他地方如类的成员变量有修改,那么这个方法就是有副作用的。 单元测试与oo并不友好的一大原因就是副作用,例如:
class A {
private int i = 0;
public void increase(){
this.i++;
}
}
如果一个单测代码要验证increase是否准确,那么必须想办法访问一个private成员变量i。当然会有很多办法,例如暴露一个测试期的桥方法getI()或者反射。但是如果变量在不那么容易访问的位置或者本身具备复杂状态时,单测代码写起来就会比较坑了。
class A{
private int i = 0;
int calculateK(int k){
return k++;
}
public void increase(){
i = calculateK(i);
}
}
改进后的方法,increase只是调用了另外一个方法,本身没有业务逻辑故可以认为是可信的。而calculateK已经不存在副作用了,只需要把各种case灌进去对比返回值,不再需要桥方法和mock。
4.2. 一个方法只做一件事
class A{
int countOfAInIndexPage(){
HttpClient client == new HttpClient();
String page = client.get("http://www.baidu.com");
int countOfA = 0;
for(Char c : page.getChar){
if(c == "A") countOfA++;
}
return countOfA;
}
}
代码很简单,读取百度首页,统计字符A的数量并返回。 但难以用单测保证质量的原因在于方法其实执行了两个动作:1,从网络读取;2,统计。第一步的问题可能导致第二步的失败,故仅仅通过一个单测无法指出是代码问题还是外部环境问题。
解决方案也很简单,将功能拆分为2个方法测试。
5 Tips
个人一些感受
过度追求覆盖率意义不大,首先对于终端来说,UI和网络等不可控因素太多难以逐个mock。其次,过度追求覆盖率会导致方法拆分过多,例如if(list != null && list.size() > 0) 也应拆成一个独立方法进行测试,如果真拆成如此细碎估计代码没人愿意维护。
TDD并未降低开发效率,只是将原本的调试测试环节前置。开发需求时大部分时间往往是在思考PRD如何转化到现有代码内。单测和业务代码开发的这个过程是完全一致的,额外的付出只是编写了一遍测试代码。但在调试和测试需求时,因为工作已经在测试代码中完成了大部分,故这部分时间可以大幅节省下来。
private方法确实没有太好的方法处理,我的解决方式是提供一个package桥方法给单测使用
mockito虽然好用,但是mock太多的代码几乎就无法维护,还是少mock为妙。