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)
测试应及时编写。单元测试应该恰好在使其通过的生产代码之前编写。