1. 程式人生 > >程式碼整潔之道 第9章 單元測試

程式碼整潔之道 第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)

測試應該有布林值輸出。你不應該檢視日誌檔案來確認測試是否通過。也不應該手工對比兩個不同文字檔案來確認測試是否通過

及時

測試應及時編寫。單元測試應該恰好在使其通過的生產程式碼之前編寫。