程式碼整潔之道 第9章 單元測試
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)
測試應該有布林值輸出。你不應該檢視日誌檔案來確認測試是否通過。也不應該手工對比兩個不同文字檔案來確認測試是否通過
及時
測試應及時編寫。單元測試應該恰好在使其通過的生產程式碼之前編寫。