Clean Code – Unit Test

By | 6月 2, 2019

1. TDD 三定律

TDD要求我们在编写生产代码前先编写单元测试,下面的三条定律只是冰山之巅。

  1. 在编写不能通过的单元测试前,不可编写生产代码。
  2. 只可编写刚好无法通过的单元测试,不能编译也算不通过。
  3. 只可编写刚好足以通过当前失败测试的生产代码。

这三条定律将你限制在大概30秒一个的循环中,测试与生产代码一起编写,测试代码只比生产代码早写几秒钟。

2. 保持测试简洁

有些开发团队,在单元测试中不做良好设计和仔细划分,变量名不用好,测试函数不必短小和具有描述性。只要测试代码还能工作,还覆盖着生产代码,就足够好。

其实他们没有意识到,脏测试等同于没测试。因为测试代码必须跟生产代码的演进而修改。测试越脏,就越难修改,你有可能花更多的时间添加新测试,而不是编写新代码。最终,编写新测试变成了开发者最大的抱怨,当经理问为何开发缓慢,开发者们就归咎于测试。最后,为了加快开发,他们只能扔掉了整个测试。没有测试,生产代码就开始变坏,客户开始抱怨。

如果保持测试简洁,测试就不会让他们失望。

测试代码和生产代码一样重要。它们可不是二等公民,它们需要被思考,被设计和被照料。

3. 简洁的测试

简洁的测试有三要素:可读性,可读性和可读性。在单元测试中,可读性甚至比生产代码还重要。

下面的测试来自FitNess,我经常写成这样,作者觉得它很难懂。


public void testGetPageHierachyAsXml() throws Exception {
  crawler.addPage(root, PathParser.parse("PageOne"));
  crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
  crawler.addPage(root, PathParser.parse("PageTwo"));

  request.setResource("root");
  request.addInput("type,", "pages");
  Responder responder = new SerializedPageResponder();
  SimpleResponse response =
      (SimpleResponse) resonder.makeResponse(new FitNessContext(root), request);
  String xml = response.getContent();

  assertEquals("text/xml", response.getContentType());
  assertSubString("PageOne", xml);
  assertSubString("PageTwo", xml);
  assertSubString("ChildOne", xml);
}


下面是重构后的测试,更简洁、更可读。


public void testGetPageHierachyAsXml() throws Exception {
  makePages("PageOne", "PageOne.ChildOne", "PageTwo");
  
  submitRequest("root", "type:pages");
  
  assertResponseIsXML();
  assertContentContains("PageOne", "PageTwo", "ChildOne");
}


4. 每个测试一个概念

有个流派认为,每个测试函数应该有且只有一个断言语句。这个有点苛刻,通常每个测试只测一个概念。

我们不想要超长的测试函数,测完这个又测那个。下面的例子测试了三个不同的是,应当拆分为三个不同的测试。


public void testAddMonths() {
  SerialDate d1 = SerialData.createInstance(31, 5, 2004);
  
  SerialDate d2 = SerialDate.addMonths(1, d1);
  assertEquals(30, d2.getDayOfMonth());
  assertEquals(6, d2.getMonth());
  assertEquals(2004, d2.getYYYY());
  
  SerialDate d3 = SerialDate.addMonths(2, d2);
  assertEquals(31, d3.getDayOfMonth());
  assertEquals(7, d3.getMonth());
  assertEquals(2004, d3.getYYYY());
  
  SerialDate d4 = SerialDate.addMonths(1, SerialData.addMonths(1, d1));
  assertEquals(30, d4.getDayOfMonth());
  assertEquals(7, d4.getMonth());
  assertEquals(2004, d4.getYYYY());
}


当多个概念被测试,就可能导致测试遗漏。

5. F.I.R.S.T原则

快速(Fast)测试应当够快。这样才能频繁运行,帮助发现代码问题。

独立(Independent)每个测试用例相互独立,以任何顺序运行。

可重复(Repeatable)测试应该能在任何环境中重复通过。

自足验证(Self-Validating)不需查看日志来确认是否通过,不需要手动比较两个文件来确认是否通过。

及时(Timely)测试应该及时编写。如果在编写生产代码之后编写测试,你就会发现生产代码难以测试。你觉得难以测试,就可能不会去写测试。(P.S. 这条感通深受)。