做更好的单元测试:关于单测你必须知道的技巧与原则
最近因工作需要不得不对单元测试中的Mockito2和Powermock框架的一些新特性进行研究:比如Mockito2和Powermock可以伪造静态方法、final类甚至是构造函数的调用,但是研究一段后发现,这些功能其实在小编本来就很熟悉的Jmockit框架中就能实现,而且不用像mockito一样需要特殊的语法和额外的样板代码,看似掌握了一些所谓“高大上”的用法,实际对工作来说没有任何收益。因此今天这篇文章不会讲什么单测框架的高级特性,反而,我们来聊一聊指导我们进行单元测试的一些基本准则。
mock还是不mock?
为什么我们需要mock来进行单元测试呢?为什么我们需要用一些假对象来替换我们测试类中的真对象呢?原因是我们想让自己的单元测试是密闭独立的,实际测试时,任何一个类都有可能依赖于其他的类,这些依赖可能来自于同一源代码的同一根目录,可能来自一些核心库(java.util.ArrayList, java.io.File),更有一大部分来自于第三方库。或许这些依赖的输出是稳定且我们可以预期的,但实际生产环境中,它们可能会依赖于文件系统、网络等这些变幻莫测的外部资源,比如任何一个使用当前日期/时间或读取硬件资源的对象,其结果对我们来说都是不可预知的,而这样的不可预知,对于单元测试来说,就是最大的bug。
在单元测试中,我们需要保证除了我们要测试的类,其“外部世界”的行为与输出与我们所预期的完全一致。举个栗子,比如我们需要测试一个service,这个service作用是根据各国的区号,计算当前国家的税率。
double taxRate = TAXService.getTAXRateForCountry(countryCode);
比如在单测case中,我们假设美国的税率是21%,但是除非我们ctrl+鼠标点击进去TAXService这个类中去查看,我们无法得知是否真的是我们所预期的21%。有可能TAXService类依赖于本地文件,也有可能会去服务器中去查,而查询本地文件,或者连接服务器这一动作,就大大降低了我们单元测试的效率。单元测试的目的就是为了快速地测试这段代码的可用性,如果想要确保整体应用于预期一致,我们要做的并非单元测试,而是集成测试、端对端测试、压力测试等。。。
“既然mock这么好,那所有的case都mock好了”
--by 从前的小编
但是,如果是下面的例子呢:
// 非mock的写法
String postCode = employeeDao
.getEmployeeById(employeeId)
.getPreviousEmployer()
.getAddress()
.getPostCode();
// mock的写法
when(employeeDao.getEmployeeById(42)).thenReturn(employee);
when(employee.getPreviousEmployer()).thenReturn(previousEmployer);
when(previousEmployer.getAddress()).thenReturn(address);
when(address.getPostCode()).thenReturn(“1234AB”);By Jove, we’re finally there!
哪个更简单明了,已经不言而喻了吧。
注释还是不注释?
在编写单元测试代码时,为了使读代码人的人清除这部分的case测试点在哪里,很重要的一部分是编写注释,有效的注释与漂亮的代码同样重要。这里要强调的是“有效“,在一些时候,过多无用的注释对文件和对你本身也是一种累赘。
如果你的代码不言自明,那就别注释了。比如下面这个例子:
// 过滤关键字
for (String word: word) {...}
// 基本税金
int taxmoney = ...;
// 扣除税费减免
finalTax = (taxItems * taxPrice)
- min(5, taxItems) * itemPrice * 0.1
下面是几个有效注释的例子:
1、解释你的意图:解释代码为什么这么做,而不是做了什么
// 最终税金 = 税金 - 减免金
2、做好todo:,避免其他人“修复”了你的代码
// TODO: 优化税费计算公式,保证小数点后3位
3、做好问题澄清,避免在code review时别人的误解
// 使用这个顺序计算税率的原因是。。。
写不写before?
请看下面这个例子:
private final Tax tax = new Tax();
@(1.4 微信公众号)Before
public void setUp() {
tax.increment("key1", 8);
tax.increment("key2",100);
tax.increment("key1",0);
}
//1000行之后
@Test
public void testIncrement_existingKey()
{
assertEquals(9, tally.get("key1"));
}
除非再把页面拉回去,我们无法得知1000行之后的单测case的结果是否正确。抽象点来说,就是原因与结果相隔太远了。
小编比较倾向的单测case的写法,是把原因与结果放在一个case中,像我们说话一样,开始为原因,结束为结果,这样的好处是代码可读性更强,维护更简单,后续同学的使用也更方便。
private final Tax tax = new Tax();
@Test
public void testIncrement_newKey() {
tax.increment("key", 100);
assertEquals(100, tax.get("key"));
}
@Test
public void testIncrement_incrementByZeroDoesNothing() {
tax.increment("key", 8);
tax.increment("key", 0);
assertEquals(8, tax.get("key"));
}
结语
其实,做单元测试的意义就在于能使测试人员得到更好地测试覆盖率,使团队得到更高的产出效率,使研发人员更自信地进行重构,如果我们写的单元测试既臃肿又不好维护,那做单测的意义还何在呢?