代码整洁之道 第9章 单元测试

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/ymybxx/article/details/83960429

1.TDD三定律

定律一 在编写能通过的单元测试前,不可编写生产代码
定律二 只可编写刚好无法通过的单元测试,不能编译也算不通过
定律三 只可编写刚好足以通过当前失败测试的生产代码

这样写程序,我们每天就会编写数十个测试,测试将覆盖所有生产代码。测试代码量将足以匹敌生产代码量,导致令人生畏的管理问题。

2.保持测试整洁

脏测试等同于没测试
测试代码和生产代码一样重要,它该像生产代码一般保持整洁

测试带来的好处

单元测试让你的代码可扩展、可维护、可复用
没有测试,每次修改都可能带来缺陷
测试覆盖率越高,就越不担心修改会造成问题

3.整洁的测试

整洁的测试最重要的要素———可读性

public void testGetPageHieratchyAsXml() 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) responder.makeResponse(new FitNesseContext(root), request);
  String xml = response.getContent();

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

public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks() throws Exception {
  WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));
  crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
  crawler.addPage(root, PathParser.parse("PageTwo"));

  PageData data = pageOne.getData();
  WikiPageProperties properties = data.getProperties();
  WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);
  symLinks.set("SymPage", "PageTwo");
  pageOne.commit(data);

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

  assertEquals("text/xml", response.getContentType());
  assertSubString("<name>PageOne</name>", xml);
  assertSubString("<name>PageTwo</name>", xml);
  assertSubString("<name>ChildOne</name>", xml);
  assertNotSubString("SymPage", xml);
}

public void testGetDataAsHtml() throws Exception {
  crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");

  request.setResource("TestPageOne"); request.addInput("type", "data");
  Responder responder = new SerializedPageResponder();
  SimpleResponse response =
    (SimpleResponse) responder.makeResponse(new FitNesseContext(root), request);
  String xml = response.getContent();

  assertEquals("text/xml", response.getContentType());
  assertSubString("test page", xml);
  assertSubString("<Test", xml);
}

这三个测试很难读懂,代码中充满了干扰测试表达力的细节。
对PathParser的那些调用,它们将字符串转换为供爬虫使用的PagePath实体。转换与测试毫无关系。
创建responder相关的细节,还有reponse的收集与转换也充满了和测试无关的细节

public void testGetPageHierarchyAsXml() throws Exception {
  makePages("PageOne", "PageOne.ChildOne", "PageTwo");

  submitRequest("root", "type:pages");

  assertResponseIsXML();
  assertResponseContains(
    "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}

public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {
  WikiPage page = makePage("PageOne");
  makePages("PageOne.ChildOne", "PageTwo");

  addLinkTo(page, "PageTwo", "SymPage");

  submitRequest("root", "type:pages");

  assertResponseIsXML();
  assertResponseContains(
    "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
  assertResponseDoesNotContain("SymPage");
}

public void testGetDataAsXml() throws Exception {
  makePageWithContent("TestPageOne", "test page");

  submitRequest("TestPageOne", "type:data");

  assertResponseIsXML();
  assertResponseContains("test page", "<Test");
}

这些测试显然呈现构造-操作-检验模式。
第一个环节构造测试数据,第二个环节操作测试数据,第三个部分检验操作是否得到期望的结果

3.1面向特定领域的测试语言

我们不应直接使用程序员用来对系统进行操作的api,而是打造了一套包装这些api的函数和工具代码,这样就能更方便的编写测试,写出来的测试也更便于阅读。

3.2双重标准

测试api中的代码与生产代码相比,的确有一套不同的工程标准。测试代码应当简单、精悍、足具表达力,但它该和生产代码一般有效

public String getState() {
  String state = "";
  state += heater ? "H" : "h"; 
  state += blower ? "B" : "b"; 
  state += cooler ? "C" : "c"; 
  state += hiTempAlarm ? "H" : "h"; 
  state += loTempAlarm ? "L" : "l"; 
  return state;
}

代码效率不是非常高。要提升效率,应该使用StringBuffer

这套应用显然是嵌入式实时系统,计算机和内存资源都很有限,不过测试环境完全不必做限制

有些事大概不会永远在生产环境中做,而在测试环境中做却完全没有问题,通常这关乎内存或cpu效率的问题。

4.每个测试一个断言

单个断言是个好准则,但也不用担心在单个测试中放入一个以上断言,最好的方法是单个测试中的断言数量应该最小化

更好一些鹅规则或许是每个测试函数中只测试一个概念。

public void testAddMonths() {
  SerialDate d1 = SerialDate.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, d1); 
  assertEquals(31, d3.getDayOfMonth()); 
  assertEquals(7, d3.getMonth()); 
  assertEquals(2004, d3.getYYYY());
  
  SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1)); 
  assertEquals(30, d4.getDayOfMonth());
  assertEquals(7, d4.getMonth());
  assertEquals(2004, d4.getYYYY());
}

这个测试应该拆解为3个单独测试

1.对于某个有31天的月份的最后一天
(1)5月31日加一个月是6月30(而非6月31日)
(2)5月31日加两个月是7月31日
2.对于某个有30天的月份的最后一天
(3)6月30加一个月是7月30日(而非7月31日)

5. F.I.R.S.T

整洁的测试还遵循以下5条规则

快速(FAST)

测试应该够快,测试缓慢你就不会想要频繁地运行它。

独立(Independent)

测试应该相互独立。某个测试不应为下一个测试设定条件。你应该可以单独的运行每个测试,及以任何顺序运行测试。

可重复(Repeatable)

测试应当可在任何环境中重复通过。

自足验证(Self-Validating)

测试应该有布尔值输出。你不应该查看日志文件来确认测试是否通过。也不应该手工对比两个不同文本文件来确认测试是否通过

及时(Timely)

测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。

猜你喜欢

转载自blog.csdn.net/ymybxx/article/details/83960429