注:正文中的引用是直接引用作者作者的话,两条横线中间的段落的是我自己的观点,其他大约都可以算是笔记了。
对于很多人来说,所谓「单元测试」只是在我们艰难地写完一些类或者方法——甚至一个工程——后,写下的一些「专用代码」来测试它们,而且这些代码往往都是一些简单的驱动程序来帮助我们和我们刚写好的程序进行交互。比如某些人写的单元测试会是这样的,单元测试跑起来后会监听键盘,然后我们键入某一个字母或者直接点击回车来执行某种测试步骤。
随着「敏捷开发」和「测试驱动开发」的流行,渐渐地人们开始转变写单元测试的方法,现在的很多人会把单元测试当做系统正常运行的保证——首先这些单元测试要保证覆盖了程序的每一个角落,然后每次这些单元测试通过,就表示我们的程序完全没有问题了。
但是随着越来越多的人急于把单元测试作为规范而去实践它,许多开发者忽略了一些非常微妙但是十分重要的点,而这些点才是你写出优雅的测试的重要因素。
在做软件开发的时候:
详细解读可以参考 Bob大叔的博客
有些人坚信「有总比没有强」,他们并没有像对待正式代码那样对待测试代码,对测试代码的要求就是「快」,往往写出来的代码却十分丑陋。比如变量并没有被良好得命名,测试代码中的方法又冗长又难懂,他们坚信测试代码不需要花那么多心思去设计,只要它们能工作就足够好了。
但是, 质量差的测试代码等同于没有测试(或者甚至比没有测试还要糟糕) 。因为我们的测试代码往往需要随着正式代码的变化而变化,如果你的测试代码写的很烂,那么你将来就要花更多的时间来修改它们,慢慢的这些写得很烂的测试代码将会成为沉重的负担。慢慢的越来越多的人开始抱怨测试代码成拖累了开发进度,进而最后放弃这些测试。
结论就是:测试代码和正式代码同样重要,我们应该像对待正式代码那样对待测试代码。
如前面几章中提到的,有了覆盖所有正式代码的单元测试,你就可以放心大胆地对系统进行修改。这就会带来许多好处: 灵活性、可维护性和可重用性 。
而丑陋的测试代码会让你的代码丧失这些好处。
好的测试有3个要素:可读性、可读性、可读性
这里有一个FitNesse中的单元测试的例子,如代码 8-1
中所示的单元测试,其中包含了大量的细节实现,可读性极差,读者想要读懂这些测试是干什么的时候就会发现自己被淹没在了无止境的细节中。
//代码8-1 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); }
在进行一番重构后,它长这个样子代码8-2。
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"); }
就像代码 8-2
中所示那样,测试代码应该是一个3段式:
我们在编写测试的时候,最好不要直接使用正式代码中的API,而是应该在它们的基础上进行包装,从而创建专用于测试的API,就像代码8-2中所示。
对测试代码的要求和正式代码并不需要完全相同,可以使用双重标准来对待它们。测试代码仍然必须要简洁而可读,但是不需要和正式代码那样拥有很高的运行效率(但同时测试也不能效率太低,跑一次测试要等很久才知道结果)。毕竟这些代码是跑在测试环境下,和正式环境有着千差万别。
效率并不是测试代码的首要目标,所以在编写测试代码时,可以牺牲一些我们在平时养成的良好习惯,而追求更好的可读性。比如在拼接字符串时(在测试代码中)就可以不使用 StringBuilder
,而直接使用 +
。
如果可能,请遵守这个规则,但它不应该成为你为了遵守它,写了一大堆冗余的代码。
在可读性良好的前提下,完全可以打破这条规则。
相比上一条,更合理的规则应该是,一个测试不应该包含多个概念的测试。比如代码 8-3
中就明显违背了此原则,这个测试要做的事情太多了,超出了一个概念的范围,应该将它分拆成3个测试。
//代码8-3 /*** Miscellaneous tests for the addMonths() method.*/ 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()); }
Fast:测试不能每跑一次都要耗费大量的时间。
Independent: 测试与测试之间不应该存在依赖关系。
Repeatable: 测试应该在任何环境下都能运行,不论是生产环境、测试环境,或者是在家里的笔记本电脑上。
Self-Validating 测试的结果应该是显而易见的,不应该是靠人去查看它的输出才能判断测试的成功与失败。
Timely: 测试需要在正式代码之前就写好。
如果你的测试代码写的很烂,那么你的正式代码也将最终变得很烂。所以请保持你的测试代码整洁。
本文链接地址: 单元测试方法论(clean code阅读笔记之八)
转载须以超链接形式标明文章原始出处和作者信息及版权声明