[学习笔记] 《代码整洁之道》— 第9章 单元测试
TDD 三定律
谁都知道TDD要求我们在编写生产代码之前先编写单元测试。
- 定律一:在编写不能通过的测试单元前,不可以编写生产代码。
- 定律二:只可编写刚好无法通过的单元测试,不能编译也算不通过。
- 定律三:只可编写刚好足以通过当前失败测试的生产代码。
保持测试整洁
- 脏测试等同于 — 如果不是坏于的话 — 没测试。
- 测试必须随生产代码的演进而修改。
- 测试越脏,就越难修改。
- 测试必须随生产代码的演进而修改。
- 测试代码和生产代码一样重要!
- 单元测试让你的代码 可扩展、可维护、可复用。
- 覆盖了生产代码的自动化单元测试程序组能尽可能地保持设计和架构的整洁。
- 测试越脏,代码就会变得越脏。最终,你丢失了测试,代码开始腐坏。
整洁的测试
-
整洁的测试有三个要素:可读性,可读性和可读性。
- 在单元测试中,可读性甚至比生产代码还重要。
- 明确,简洁,足够的表达力
-
先看来自 FitNesse 的代码:
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 reponse = (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 reponse = (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 reponse = (SimpleResponse) responder.makeResponse( new FitNesseContext(root), request); String xml = response.getContent(); assertEquals("text/xml", response.getContentType()); assertSubString("test page", xml); assertSubString("<Tset", xml); }
重构为更整洁和有表达力的形式:
public void testGetPageHieratchyAsXml() throws Exception{ makePage("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"); makePage("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{ makePageWidthContent("TestPageOne", "type:data"); submitRequest("TestPageOne", "type:data"); assertResponseIsXML(); assertResponseContains("test page", "<Text"); }
这些测试显然呈现了构造-操作-检验 (BUILD-OPERATE-CHECK)模式。
- 第一个环节构造测试数据;
- 第二个环节操作测试数据;
- 第三个环节检验操作是否得到期望的结果。
-
双重标准:测试 API 中的代码与生产代码相比。的确有一套不同的工程标准。
-
有些事你大概永远不会在生产环境中做,而在测试环境中却完全没问题。
-
例:测试在“温度太低”时检验温度报警器、加热器和送风机是否全部打开。
@Test public void turnOnLoTempAlarmAtThreashold() throws Exception{ hw.setTemp(WAY_TOO_COLD); controller.tic(); asserTure(hw.heaterState()); asserTure(hw.blowerState()); asserFalse(hw.coolerState()); asserFalse(hw.hiTempState()); asserTure(hw.loTempState()); }
重构后:
@Test public void turnOnLoTempAlarmAtThreashold() throws Exception{ wayTooCold(); assertEquals("HbchL", hw.getState()); }
- 创建了一个 wayTooCold 函数,隐藏了 tic 函数的细节。
- assertEquals 中那个奇怪的字符串:大写表示“打开”,小写表示“关闭”。
- {heater, blower, cooler, hi-temp-alarm, lo-temp-alarm}
扩展到更大范围:
@Test public void turnOnCoolerAndBlowerIfTooHot() throws Exception{ tooHot(); assertEquals("hBChl", hw.getState()); } @Test public void turnOnHeaterAndBlowerIfTooClod() throws Exception{ tooHot(); assertEquals("HBchl", hw.getState()); } @Test public void turnOnHiTempAlarmAtThershold() throws Exception{ wayTooHot(); assertEquals("hBCHl", hw.getState()); } @Test public void turnOnLoTempAlarmAtThershold() throws Exception{ wayTooCold(); assertEquals("HBchL", hw.getState()); } 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; }
-
-
尽可能减少每个概念的断言数量,每个测试函数只测试一个概念。
-
F.I.R.S.T.
- 快速(Fast):测试应该足够快,应该能快速运行。
- 独立(Independent):测试应该相互独立。
- 某个测试不应为下一个测试设定条件。
- 可以单独运行每个测试,及以任何顺序运行测试。
- 可重复(Repeatable):测试应该可在任何环境中重复使用。
- 自足验证(Self-Validating):测试应该有布尔值输出。
- 不应该查看日志文件来确认测试是否通过。
- 不应该手工对比两个不同文本文件来测试是否通知。
- 及时(Timely):测试应及时编写。
参考文献
[1] Robert C. Martin 著,韩磊 译,《代码整洁之道》,北京:人民邮电出版社,2010.1(2018.9 重印), ISBN 978-7-115-21687-8。