ATDD實戰

ATDD實戰

來自專欄 軟體測試全知道

驗收測試驅動開發入門

你是否遇到過這樣的場景:

QCon全球軟體開發大會(北京站)2014,4月25-27日,誠邀蒞臨。

那麼本文就是為您而作——以一個具體的例子闡述了如何基於已有的代碼庫啟用驗收測試驅動開發(acceptance-test driven development)。這是應對技術債解決方案的一部分。

這是帶有一定缺陷的現實世界的樣例,並不像教科書中的樣例那樣完美。所以完全是來自於實戰。我只會使用Java與Junit,而沒有使用其他的第三方測試框架(這些框架基本上已經被過度使用了)。

免責聲明:我並不是說這就是正確地方式,關於ATDD有很多其他的「偏好(flavors)」 。同時,本文中也沒有太多新鮮的或革新性的內容,它只是一些確定的實踐以及通過辛苦得來的經驗。

我想做什麼

幾天前,我開始為webwhiteboard.com(我的小項目)添加密碼保護的特性。很久以來,人們就希望有一種密碼保護在線白板的方式,現在該實現這個功能了。

聽起來這是一個很簡單的特性,但是需要做很多的設計決策。到目前為止,webwhiteboard.com是基於匿名使用的,沒有任何的賬戶、登錄或密碼這樣的東西。什麼人能夠保護白板呢?誰能訪問它呢?如果我忘記密碼該怎麼辦?我們如何在足夠安全的同時保證儘可能簡單?

webwhiteboard的代碼庫有著很棒的單元測試和集成測試的覆蓋率。但是它並沒有驗收測試,也就是站在用戶的角度進行端對端流程的測試。

設計要素

webwhiteboard的主要設計目標是很簡單的:儘可能簡化登錄、賬戶以及其他繁瑣事情的需求。所以我為密碼特性建立了兩個設計限制:

對白板設置密碼需要用戶認證,但是訪問密碼保護的白板並不需要。也就是說,用戶訪問保護的白板需要輸入密碼,而不需要「登錄」。

登錄會使用第三方的OpenId/Oauth服務提供商,最初會使用Google。按照這種方式,用戶就不需要再創建賬號了。

實現方式

這裡有很多尚未確定的事情:我並不確定它會如何工作,更不確定如何去實現它。因此,以下就是我的實現方式(基本的ATDD):

步驟1:在較高的層面編寫預計的流程。

步驟2:將其轉化為可執行的驗收測試。

步驟3:執行驗收測試,但是會失敗。

步驟4:使得驗收測試成功執行。

步驟5:清理代碼。

這是一個迭代式的過程,所以每一步中我都可能會返回上一步進行調整(這是我經常做的事情)。

步驟1:編寫預計的流程

假設這個特性已經完成了。在我睡覺的時候有個天使來了並實現了這個特性。這聽起來美妙得難以令人置信!那我該如何對其進行檢驗呢?要進行手工測試的話,我首先要做什麼呢?應該是:

我創建一個新的白板。

我對其設置一個密碼。

Joe試圖打開我的白板,被要求輸入密碼。

Joe輸入錯誤的密碼,被拒絕訪問。

Joe再次嘗試輸入正確的密碼,可以進行訪問。(當然,「Joe」就是我自己,只不過使用另外一個瀏覽器……)。

當我編寫完這個小的測試腳本後,我意識到要考慮很多可選的流程。但這就是主要的場景。如果我能夠讓它運行起來,就已經取得了很大的進展。

步驟2:將其轉化為可執行的驗收測試

這是一個需要技巧的步驟。我並沒有其他端到端的驗收測試,所以我該如何開始呢?這個特性會與第三方的認證系統(我最初的選擇是使用Janrain)以及資料庫交互,並且這裡還有Web相關的內容,包括彈出對話框、令牌(token)以及重定向等等。

現在該退後一步。在解決「我該如何編寫這個驗收測試」之前,我首先要解決一個更為基本的問題,那就是「基於這個代碼庫,我到底該怎樣編寫驗收測試呢?」

為了推進這個問題,我試圖識別可以進行測試的「儘可能簡單的特性」,這就是今天已經可用的一些特性。

步驟2.1 編寫儘可能簡單的可執行驗收測試

以下就是我能夠想到的測試步驟:

1.試圖打開一個不存在的白板

2.檢查確認無法得到白板

我該如何實現這個測試呢?使用什麼框架?什麼工具?它是否應該涉及到GUI,或者忽略它?是否涉及到客戶端代碼還是直接與伺服器進行交流?

有很多的問題。技巧在於:不要回答這些問題!只要假設這些問題已經以某種方式很漂亮地解決了,並將測試編寫為偽代碼的形式。如下。

public class AcceptanceTest { @Test public void openWhiteboardThatDoesntExist() { //1. 試圖打開一個不存在的白板 //2. 檢查確認無法得到白板 } }

我運行它,並且成功了!太棒了!但是稍等一下,這是錯誤的!在TDD三角(「紅-綠-重構」)中的第一步是紅色。所以,我需要讓這個測試失敗,以證明這是一個需要實現的特性。

我會編寫一些真正的測試代碼。不過,這些偽代碼能夠保證我的方向是正確的。

步驟2.2 將儘可能簡單的驗收測試變為紅色

為了將其變成真正的測試,我構建了一個名為AcceptanceTestClient的類,我假裝它已經魔法般地解決了所有的問題並且為我提供了漂亮的、高級的API來運行驗收測試。它的使用很簡單,如下:

client.openWhiteboard("xyz"); assertFalse(client.hasWhiteboard());

當我編寫這些代碼的時候,創建了一個API來適應這個測試用例的需求。它應該與偽代碼的行數差不多。

接下來,我使用Eclispe的快捷鍵自動生成空白版本的AcceptanceTestClient以及我所需要的方法:

public class AcceptanceTestClient { public void openWhiteboard(String string) { // TODO Auto-generated method stub }

public boolean hasWhiteboard() {

// TODO Auto-generated method stub

return false;

}

}

以下是完整的測試類:

public class AcceptanceTest { AcceptanceTestClient client;

@Test

public void openWhiteboardThatDoesntExist() {

//1. 試圖打開一個不存在的白板

client.openWhiteboard("xyz");

//2. 檢查確認我無法得到白板

assertFalse(client.hasWhiteboard());

}

}

運行測試,但是它會失敗(因為客戶端為null)。很好!

我都做了些什麼呢?其實沒有太多。但這是一個起點。驗收測試的幫助類AcceptanceTestClient已經有了雛形。

步驟2.3 將儘可能簡單的驗收測試變為綠色

下一步就是將這個驗收測試變為綠色。

我現在所要解決的是一個更為簡單的問題。我不需要關心認證以及多用戶等等的問題。稍後我可以為這些問題添加測試。

對於AcceptanceTestClient,實現很標準——模擬資料庫(我已經有這樣的代碼了)並運行一個內存版本的完整的webwhiteboard系統。

以下為生產環境的設置:

技術細節:Web Whiteboard使用了GWT(Google Web Toolkit)。任何事情都是使用Java編寫的,但是GWT會自動將客戶端代碼轉換為JavaScript,並插入RPC(Remote Procedure Calls)的邏輯,從而封裝非同步客戶端-伺服器通信的繁瑣細節。

在驗收測試的環境中,我對系統進行了「短路(short circuit)」,移除掉了所有的框架、第三方服務以及網路通信。

我創建了AcceptanceTest客戶端,它會與web whiteboard服務進行交互,就像真實的客戶端一樣。區別都是在幕後的:

1.真正的客戶端與web whiteboard服務介面進行交互,它會運行在GWT環境之中,這個環境會將請求轉換為RPC調用並轉發到伺服器端。

2.驗收測試客戶端也會與web whiteboard服務介面進行交互,但是它會直接連接到本地實現中,運行測試時,沒有必要進行RPC調用,因此也沒有必要使用GWT。

同時,驗收測試配置將mongo資料庫(基於雲的NoSQL資料庫)替換為虛擬的內存資料庫。

這種虛擬的原因在於簡化環境、讓測試運行得更快並且確保獨立於框架和網路因素來測試業務邏輯。

看起來這似乎是一個很複雜的環境搭建過程,但實際上只是包含3行代碼的初始化方法。

public class AcceptanceTest { AcceptanceTestClient client;

@Before

public void initClient() {

WhiteboardStorage fakeStorage = new FakeWhiteboardStorage();

WhiteboardService service = new WhiteboardServiceImpl(fakeStorage);

client = new AcceptanceTestClient(service);

}

@Test

public void openWhiteboardThatDoesntExist() {

client.openWhiteboard("xyz");

assertFalse(client.hasWhiteboard());

}

}

WhiteboardServiceImpl是web whiteboard系統中已有的服務端實現。

注意AcceptanceTestClient的構造函數中接受一個WhiteboardService實例(這種模式稱之為」依賴注入「)。這種方式給我們帶來了一種便利:它不關心配置。相同的AcceptanceTestClient可以用於真實環境的測試,只需將真實配置的WhiteboardService實例傳遞給它即可。

public class AcceptanceTestClient { private final WhiteboardService service; private WhiteboardEnvelope envelope;

public AcceptanceTestClient(WhiteboardService service) {

this.service = service;

}

public void openWhiteboard(String whiteboardId) {

boolean createIfMissing = false;

this.envelope = service.getWhiteboard(whiteboardId, createIfMissing);

}

public boolean hasWhiteboard() {

return envelope != null;

}

}

總而言之,AcceptanceTestClient模擬了真實的web whiteboard客戶端所做的事情,同時又為驗收測試提供了較高層次的API。

你可能想知道,「既然我們已經有了WhiteboardService可以進行直接交互,為什麼還要AcceptanceTestClient呢?」。這裡有兩個原因:

WhiteboardService API是更為低層次的,而AcceptanceTestClient就是驗收測試所需要的,並且能夠使它儘可能地易讀。

AcceptanceTestClient隱藏了測試代碼不需要的內容,如WhiteboardEnvelope的概念、createIfMissing布爾值以及其他低層次的細節。在現實中,會涉及到更多的服務,如UserService和WhiteboardSyncService。

我不會向你過多地闡述AcceptanceTestClient的細節,因為本文不會探究web whiteboard的內部實現。簡單來說,AcceptanceTestClient將與白板服務介面交互的低層次細節匹配到驗收測試的需要上。這很容易實現,因為真正的客戶端代碼可以作為如何與服務進行交互的教程。

到此為止,我們儘可能簡單的驗收測試可以通過了!

@Test public void openWhiteboardThatDoesntExist() { myClient.openWhiteboard("xyz"); assertFalse(myClient.hasWhiteboard()); }

下一步要進行一些清理。

實際上,我並沒有為此編寫任何的生產環境代碼(因為這些特性已經存在並且可用),這是測試框架的代碼。我需要花幾分鐘的時間對其進行清理、移除重複內容、讓方法名更為整潔等。

最後,我又添加了一個測試,只是為了完整性,而且它確實很簡單。

@Test public void createNewWhiteboard() { client.createNewWhiteboard(); assertTrue(client.hasWhiteboard()); }

非常好,我們有了一個測試框架!我們甚至沒有使用任何第三方的庫,只是Java和Junit。

步驟2.4 為密碼保護特性編寫驗收測試代碼

現在,該對我的密碼保護特性添加測試了。

首先,我將最初的「規範(spec)」複製為偽代碼:

@Test public void passwordProtect() { //1. 我創建一個新的白板。 //2. 我對其設置一個密碼。 //3. Joe試圖打開我的白板,被要求輸入密碼。 //4. Joe輸入錯誤的密碼,被拒絕訪問。 //5. Joe再次嘗試輸入正確的密碼,可以進行訪問。 }

現在,我再次編寫測試代碼,假設AcceptanceTestClient已經具備了所有需要的東西,並且完全按照我要求的方式,我發現這種技術是相當有用的。

@Test public void passwordProtect() { //1. 我創建一個新的白板。 myClient.createNewWhiteboard(); String whiteboardId = myClient.getCurrentWhiteboardId();

//2. 我對其設置一個密碼。

myClient.protectWhiteboard("bigsecret");

//3. Joe試圖打開我的白板,被要求輸入密碼。

try {

joesClient.openWhiteboard(whiteboardId);

fail("Expected WhiteboardProtectedException");

} catch (WhiteboardProtectedException err) {

//Good

}

assertFalse(joesClient.hasWhiteboard());

//4. Joe輸入錯誤的密碼,被拒絕訪問。

try {

joesClient.openProtectedWhiteboard(whiteboardId, "wildguess");

fail("Expected WhiteboardProtectedException");

} catch (WhiteboardProtectedException err) {

//Good

}

assertFalse(joesClient.hasWhiteboard());

//5. Joe再次嘗試輸入正確的密碼,可以進行訪問。

joesClient.openProtectedWhiteboard(whiteboardId, "bigsecret");

assertTrue(joesClient.hasWhiteboard());

}

這個測試代碼只需要幾分鐘就能編寫完成,因為我可以在進一步編寫代碼的時候再將這些邏輯組織起來。這些方法在AcceptanceTestClient中幾乎都(還)不存在。

當我編寫這些代碼的時候,我需要做出一些設計決策。不要費力去想,做第一時間進入你腦海的事情。完美是足夠好的敵人,現在,我已經足夠好了,也就是一個可運行的失敗的測試用例。稍後,當運行測試變成綠色時,我再進行重構並進一步思考設計。

現在就進行重構是一件很有誘惑力的事情,尤其是重構這些醜陋的try/catch語句。但是TDD規約中有一點就是在進行重構之前要首先將其變成綠色,因為測試會保護你的重構。所以我決定先暫時等待一下再進行清理。

步驟3 執行驗收測試,但是會失敗

按照測試三角,下一步要運行測試,但是會失敗。

同樣,我使用Eclipse快捷鍵來創建缺失方法的空白版本。很好!運行測試,看,出現了紅色!

步驟4:將驗收測試變為綠色

現在,我需要編寫一些生產級別的代碼。我為系統添加一些新的概念,有一些所添加的代碼並不是試驗性的,因此需要進行單元測試。我使用了TDD的方式,它與ATDD類似,但是範圍更小一些。

以下展現了ATDD和TDD如何組合在一起。可以將ATDD視為外部的循環:

對於每個驗收測試循環(在特性級別)的迴路中,我們都會有很多單元測試的迴路(在類和方法級別)。

所以,儘管我在較高的層次上關注於將驗收測試變為綠色(這可能會耗費幾個小時的時間),但是在較低的層次上我可能會關注於將下一個單元測試變為紅色(這可能只會耗費幾分鐘的時間)。

這並不是非常嚴格的TDD(Leather & Whip TDD)。 這更像是「至少要保證單元測試與生產級別的代碼是同時提交的」。這種提交每小時會發生多次,大致就可以將其稱之為TDD了。

步驟5:清理代碼

像通常那樣,在驗收測試變成綠色之後,就要進行清理工作了。不要試圖越過這個步驟!就像在飯後清洗餐具一樣——需要馬上去做。

我不僅清理了生產環境中的代碼,還清理了測試代碼。例如,我將凌亂的try-catch部分抽取到一個幫助方法之中,從而最終實現了漂亮且整潔的測試方法:

@Test public void passwordProtect() { myClient.createNewWhiteboard(); String whiteboardId = myClient.getCurrentWhiteboardId();

myClient.protectWhiteboard("bigsecret");

assertCantOpenWhiteboard(joesClient, whiteboardId);

assertCantOpenWhiteboard(joesClient, whiteboardId, "wildguess");

joesClient.openProtectedWhiteboard(whiteboardId, "bigsecret");

assertTrue(joesClient.hasWhiteboard());

}

我的目標是讓驗收測試儘可能簡短、整潔並且易於使用,以至於注釋都是多餘的。最初的偽代碼或注釋會作為模板——「我希望代碼就是如此得簡潔!」。移除注釋會給我一種成就感,它的一個積極作用就是讓方法更加簡短了。

下一步做什麼?

重複地進行凈化。在第一個測試用例通過之後,我就要開始思考缺失了什麼。例如,密碼保護應該還需要用戶認證。所以,我為此添加一個測試、使其變紅色、再變成綠色然後進行清理。諸如此類。

以下就是我(到目前為止)為該特性所添加的完整的測試:

passwordProtectionRequiresAuthentication()protectWhiteboardpasswordOwnerDoesntHaveToKnowThePasswordchangePasswordremovePasswordwhiteboardPasswordCanOnlyBeChangedByThePersonWhoSetIt

當發現缺陷或添加新特性時,我稍後肯定會添加新的測試。

我總共用了大約兩天的時間進行高效地編碼。在這個過程中,有很大一部分是回過頭去重新編碼和設計,並不像本文所展示那樣線性進行。

那手工測試呢?

在自動化測試變成綠色後,我也會進行很多的手工測試。但鑒於自動化測試已經覆蓋了基本的功能和很多邊界場景,因此手工測試可以更多地關注主觀性和探查性的內容。高水平的用戶體驗是什麼樣的?流程合理嗎?它易於理解嗎?我需要在什麼地方添加幫助文本?按照美學,設計是否可接受?我不想去爭取什麼設計大獎,但我也不想讓它很醜陋。

強大的驗收測試能夠讓我們不必再進行單調且重複性的手工測試(也被稱為「搞怪測試monkey testing」),進而節省出時間來進行更有意思和更有價值的手工測試。

理想情況下,我應該在開始階段就構建驗收測試,所以一定程度上來講這種方式是在償還技術債。

關鍵點

就這樣,我希望這個樣例對你有用!它闡述了一種典型的場景——「我要實現新的特性,最好要編寫驗收測試,但是到目前為止還沒有這樣的測試,我不知道該使用什麼框架,甚至不知道該如何開始」。

我非常喜歡這種模式,藉助這種方式我多次走出了困境。總結如下:

在便利的幫助類(在我的場景中也就是AcceptanceTestClient)背後假設封裝了複雜的框架。

為已經可以運行的特性編寫非常簡單的驗收測試(如只是打開應用)。使用它來驅動你的AcceptanceTestClient實現以及相關的測試配置(如假的資料庫連接和其他外部服務)。

為新的特性編寫驗收測試。運行它,但是會失敗。

使其變成綠色。在編碼的過程中,對所有非試驗性的內容編寫單元測試。

重構。可能會額外編寫更多的單元測試或移除多餘的測試。保持代碼的整潔!

完成這些後,你就已經越過了最困難的門檻,已經開始了ATDD!


推薦閱讀:

Xebium詳解06-Web自動化測試
Selenium 2.0與Selenum 3.0介紹
Fiddler抓包工具初識
我是如何使用python來確定理財策略的
【軟體測試】如何進行APP兼容性測試

TAG:測試驅動開發 | 軟體測試 |