軟件測試技術,在現代軟件工程中變得愈發的重要,單元測試、集成測試、自動化測試等測試技術都可以大幅度提高軟件產品的質量,降低軟件開發成本。
軟件開發過程中,最基本的測試就是單元測試。在現代軟件工程中,單元測試已經是軟件開發不可或缺的一部分。良好的單元測試技術對軟件開發至關重要,可以說它是軟件質量的第一關,是軟件開發者對軟件質量做出的承諾。敏捷開發中尤其強調單元測試的重要性。
單元測試需要遵循特定規則,違反了這些規則,便失去了單元測試的意義。這些單元測試規則有:
單元測試的時間應該非常短,這樣就可以向開發者快速反饋信息。這個要求其實非常的高,特別是在測試驅動這種開發模式中,快速高效的單元測試能夠極大的提高開發、重構代碼的速度進而提高和改善軟件的設計。
還可以反過來看單元測試的規則定義,如果一個測試滿足下列定義的任何一個,它就不是一個真正的單元測試:
單元測試中如果訪問數據庫,網絡,文件系統,將會極大的影響單元測試的執行效率,執行時間一般會因 IO 操作而增加, 從而使單元測試變得太久而不可忍受,開發人員一般希望能夠快速反饋測試結果。比如重構了代碼后第一步就是運行 單元測試,看有多少測試案例因代碼的改變而受到了影響,如果此時測試用例的運行時間過于長久,會失去敏捷開發的敏捷性,進而影響開發進度。
隨著產品的復雜性增加,功能增加,要覆蓋更多的邏輯,單元測試代碼勢必變得更加復雜龐大,單元測試用例的簡潔和獨立性就變的愈發重要,高效的單元測試代碼對開發者提出了更高的要求。單元測試邏輯的任何對第三方的直接依賴如數據庫,網絡,文件系統都會降低單元測試的效率和速度。
為滿足以上單元測試的要求,通過一定的方法和技巧,解脫單元測試對外界的依賴變得更有現實意義。良好的單元測試代碼會極大的改善軟件代碼的架構設計和幫助開發人員編寫可測試的代碼(Testable Code),提高軟件質量。
測試替代技術就是這樣一種方式,它可以幫助單元測試人員擺脫對第三方系統的依賴,進而提高單元測試的隔離性和執行效率。
從單元測試的規則看,對單元測試的要求是很高的,特別是復雜系統,高效的單元測試案例本身,也對軟件開發者提出了更高的要求。編寫單元測試代碼,意味著要求開發者編寫可測試的代碼,可測試的代碼隱含著良好的代碼設計。
隔離的單元測試意味著把單元測試中的對第三方系統依賴的部分合理的提取出來,用替代體(Test Double)取而代之,使單元測試把注意力集中放在測試“單元”的邏輯上而不是和第三方系統的交互上。
現實開發中,開發人員會用不同類型的測試替代技術去隔離測試,這些測試替代技術如圖 1 所示,一般包括:假體, 存根,模擬體和仿制體。這些類別的測試替代技術各有自己優點和缺點。下面將介紹每個測試替代技術,并討論他們使用的范圍。
假體是真正接口或抽象類的實現體(Implementation),它是對父類或接口的擴展和實現。假體實現了真正的邏輯,但它的存在只是為了測試,而不適合于用在產品中。
比如有個簡單的 Logger 類,它可以把日志寫到文件系統或是數據庫中。下面是對應的設計類圖。從設計可以看出 Logger 依賴于 Writer 接口,Writer 接口有兩個實現 FSWriter 和 DBWriter,分別對應著寫文件和寫數據庫, 類圖如圖 2 所示。
Writer 通過 Logger 的構造函數注入到 Logger 實例中,此時如果想測試 Logger.logFormatedMsg()單元,為了實例化 Logger,我們可以如清單 1 所示實現 Writer 的假體 FakeWriter 類,然后注入到 Logger 中去,FakeWriter 對象的 write 函數被調用時 log 沒有寫入文件系統而是保存在變量 msg 中,隔離了文件訪問,保存的 msg 可以用來驗證 msg 是否符合測試的期望。
1
2
3
4
5
6
7
8
9
10
|
public class FakeWriter implements Writer { private String msg = null; @Override public void write(String log) { this.msg = log; } public String getMsg() { return msg; } } |
存根是當存根的方法被調用的時候,傳遞間接的輸入給調用者。存根的存在僅僅是為了測試。存根可以記錄一些其它的信息,如調用的次數,調用的參數等信息。比如測試中異常的處理等,忽略輸入的參數而只是拋出異常以測試單元的異常處理功能。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class StubWriter implements Writer { @Override public void write(String msg) throws IOException { throw new IOException("IO errors"); } } @Test(expected = IOException.class) public void test_log_ioException_error() { StubWriter stubWriter = new StubWriter(); Logger logger = new Logger(stubWriter); String msg = "log out messages.."; logger.logandFormatMsg(msg); } |
如清單 2 所示,在這個測試中,StubWriter 的 write 函數忽略了輸入參數,用 Stub 只返回測試想要的測試預期,進而測試 Logger 處理異常是不是符合期望。
仿制體是在程序中不真實存在的對象,只是為了測試的目的而“制造”的一個虛擬對象,這個制造的仿制體對測試的邏輯幾乎沒有影響,只是為了滿足測試對象實例化時的依賴要求。清單 3 所示,dummyCustomer 是不真實存在的對象。
1
2
3
4
5
6
7
|
@Test public void test_how_many_customer_serviced() { Customer dummyCustomer=new Customer("aname","male"); DriverSvr service=new DriverSvr(); service.take(dummyCustomer); assertEquals(1,service.getCountOfCustomer()); } |
dummyCustomer 只是為了作為數據參數滿足 DriverSvr 實例的 take 函數被調用,其實對我們要測試的邏輯“服務的人數”,幾乎沒有直接的影響。
模擬體本身有期望,期望是測試者賦予模擬體的。比如測試從模擬體期望一個值,在模擬體的某個方法被調用時要返回這個期望值。模擬體還可以記錄一些其他的信息,如某個函數被調用的次數等等。模擬體框架有 JMock,EasyMock,Mockito 等,他們各有特點,但功能是相同的,都是提供模擬體以幫助測試。下面的例子用的是 Mockito,它的語法和語義使用更簡單。Mockito 也可以提供存根 Stub 的功能,定在 org.mockito.stubbing 中,此處不再贅述。
以 Mock 測試 Logger 為例,如清單 4 所示,FSWriter 的模擬體 mockedWriter 被注入到 Logger 的實例中。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Test public void test_logger_formatandLogging() throws IOException { Writer mockedWriter = mock(FSWriter.class); Logger logger = new Logger(mockedWriter); ArgumentCaptor< String > captor = ArgumentCaptor.forClass(String.class); String msg = "theMsg"; logger.logandFormatMsg(msg); verify(mockedWriter).write(captor.capture()); verify(mockedWriter, new Times(1)).write(anyString()); String expectedFormatedMsg = "warning-" + msg; assertEquals(expectedFormatedMsg, captor.getValue()); } |
測試時,用 Mockito 的 ArgumentCaptor 截取要寫入文件的信息用于驗證日志和其格式是否符合期望,從而驗證了 logger 的格式化邏輯。
由以上的分析可以看出,模擬體 (mock) 功能最為強大和全面,是現代單元測試中最常用的一種測試輔助隔離技術。
以上主要討論的是常用的測試替代技術,以下我們將討論一些在設計單元測試過程中,經常遇到的一些需要隔離的單元測試。
要替代單元測試中的可替代體,首先讓我們來分析一下,都有哪些“第三方“需要被隔離,然后再分別有針對性的討論具體的“替代”方法。
可明確識別的對第三方訪問的有,網絡訪問,數據庫訪問,第三方類庫和文件系統,下面分析一下他們各自的特點以及對應的可行的替代技術:
軟件產品訪問網絡,已經變的更加普遍,隨著現代軟件技術的發展,軟件再也不是孤立的個體,軟件產品需要各種網絡服務來滿足當前軟件的功能需要,無論是桌面型應用還是基于瀏覽器的 B/S 架構的軟件,幾乎不可避免的要訪問網絡。
特別是最近幾年的基于服務的軟件架構技術的流行,軟件程序中不得不處理 SOAP,RESTful,Socket 等等的網絡訪問。按照單元測試的規則,單元測試中這種對網絡的訪問應該被隔離開來,以提高測試效率。我們以 RESTFul 為例,看如何在單元測試中通過合理的設計來隔離其對網絡的訪問。
這個例子是用 IBM Cognos 中提供的 RESTful 的接口去提取中報表中的數據,報表中的數據可以用 GET 方式訪問,以 RESTful 的方式獲取,例如:
其中 QueryString 中 LDX 定義了數據的格式,一種 XML 格式輸出,selection 選擇只取報表中的 List1 中的數據。i7E932A825B08459C832B72EFC608C0FE 是 CM 中報表的存儲 ID,可以通過 CMQuery 工具獲取。LDX 的輸出中摻雜著格式化的信息,上圖 3 是 CognosBI 的 Report 的輸出。
此處的目標只是獲取其中的數據,所以在獲取 LDX 格式的數據后,要通過 XPath 的方式抽取其中的數據部分然后轉換成另外一種可以通過行列存取的格式,數據抽取的部分邏輯和網絡訪問定義在不同的實現類中,他們之間的接口是抽象類 InputStream,在單元測試中數據轉換部分的測試需要隔離。相應的類圖如圖 4 所示,DataConvert 依賴于接口 CognosClient,CognosClientService 實現了接口 CognosClient,是接口的具體實現類。
如程序清單 5 所示,測試中可以看到測試數據是直接嵌入到程序中,模擬體 mockedClient 被調用時,直接返回了嵌入的測試數據,而沒有去訪問網絡,實現了對網絡訪問的隔離。(注:數據也可以以資源的方式直接嵌入到 Jar 中,然后用 this.getClass().getResourceAsStream() 加載數據,不過這似乎是間接訪問了文件系統。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@Test public void test_cognos_list_data_converter_with_mockedClient() { CognosClient mockedClient = mock(CognosClient.class); DataConverter converter = new DataConverter(mockedClient); when(mockedClient.getCognosStream()).thenReturn(this.getLocalData()); ArrayList< ArrayList <String>> data = converter.convert(); verify(mockedClient,new Times(1)).getCognosStream(); assertEquals(1, data.size()); // for simple,only columns inserted } private InputStream getLocalData() { String content = "< filterResultSet xmlns = 'http://www.ibm.com/xmlns/prod/cognos/layoutData/200904' >...."; ByteArrayInputStream is = new ByteArrayInputStream(content.getBytes()); BufferedInputStream bstream = new BufferedInputStream(is); return bstream; } |
現今數據庫訪問中,特別是商業軟件,數據庫訪問是系統的一部分。在軟件中,一般會通過數據庫提供的類庫來訪問數據,還用一些通用的標準如 JDBC 等提供對數據庫的訪問規范和實現。軟件架構設計時,數據的訪問會被抽象到持久層中,在這個持久層中,會把實體和對象通過 ORM 框架相互映射,如 OpenJpa 就是這樣一個框架,可以幫助開發者很容易的實現對象和數據庫實體之間的轉換,避免了開發者直接以寫 Sql 的方式訪問數據。
單元測試中應避免直接訪問數據庫,數據庫的訪問可以通過模擬體 (Mock) 對象輕松隔離開。如我們有個 UserDao 類,這個類實現了對 User 的增刪改查,可以通過 userDao=mock(UserDao.class) 和 when() 等,把所有的對通過這個類實例訪問數據庫的方法截獲并返回自己“制造”的對象或數據,從而隔離和避免了對數據庫的直接訪問。
還有些數據庫實現了內存數據庫的概念,如嵌入式數據庫 Derby,Sqlite,H2 等,單元測試中對這類數據庫的訪問利用其嵌入式接口都可以在內存完成,沒有額外的配置要求。
文件系統的訪問,通過模擬體(Mock)的方式,可以模擬幾乎所有文件的 IO 操作,如 Logger 測試中,Writer mockedWriter = mock(FSWriter.class),在 Logger 寫出數據到文件時,寫出的操作 write 被截獲,從而避免了對文件系統的訪問。
根據單元測試的規則,單元測試中應避免對文件系統,數據庫系統,網絡系統的訪問,因為這些訪問意味著需要額外的配置(對第三方的依賴如文件路徑,數據庫鏈接,網絡服務器連接等等),進而使單元測試的效率降低。假體,存根,仿制和模擬技術可以用于滿足這些要求,其中模擬技術功能最為全面,可以非常有效的隔離單元測試。單元測試不僅能夠提高代碼質量,優化代碼設計,同時也提高了開發人員的代碼水平,節省了開發成本,是軟件開發過程中不可或缺的重要組成部分。
原文轉自:https://www.ibm.com/developerworks/cn/java/j-lo-TestDoubles