<ruby id="h6500"><table id="h6500"></table></ruby>
    1. <ruby id="h6500"><video id="h6500"></video></ruby>
          1. <progress id="h6500"><u id="h6500"><form id="h6500"></form></u></progress>

            如何通過(guò)測試替代(Test Doubles)合理隔離單元測試以提高單元測試效率

            發(fā)表于:2017-12-27來(lái)源:IBM作者:夏 建東 和 尹 鵬點(diǎn)擊數: 標簽:
            軟件開(kāi)發(fā)過(guò)程中,最基本的測試就是單元測試。在現代軟件工程中,單元測試已經(jīng)是軟件開(kāi)發(fā)不可或缺的一部分。良好的單元測試技術(shù)對軟件開(kāi)發(fā)至關(guān)重要,可以說(shuō)它是軟件質(zhì)量的第一

            單元測試意義

            軟件測試技術(shù),在現代軟件工程中變得愈發(fā)的重要,單元測試、集成測試、自動(dòng)化測試等測試技術(shù)都可以大幅度提高軟件產(chǎn)品的質(zhì)量,降低軟件開(kāi)發(fā)成本。

            軟件開(kāi)發(fā)過(guò)程中,最基本的測試就是單元測試。在現代軟件工程中,單元測試已經(jīng)是軟件開(kāi)發(fā)不可或缺的一部分。良好的單元測試技術(shù)對軟件開(kāi)發(fā)至關(guān)重要,可以說(shuō)它是軟件質(zhì)量的第一關(guān),是軟件開(kāi)發(fā)者對軟件質(zhì)量做出的承諾。敏捷開(kāi)發(fā)中尤其強調單元測試的重要性。

            單元測試規則

            單元測試需要遵循特定規則,違反了這些規則,便失去了單元測試的意義。這些單元測試規則有:

            1. 單元測試應該無(wú)依賴(lài)和隔離
              如測試類(lèi) ATest.java 和測試 BTest.java 必須不能相互依賴(lài),無(wú)論是先運行測試 ATest 還是先運行 BTest,對測試結果都不應該有任何影響。
            2. 易于安裝及運行
              單元測試的執行不應該需要配置等繁瑣操作就可以運行。如果單元測試代碼包含訪(fǎng)問(wèn)數據庫、網(wǎng)絡(luò )等,這個(gè)測試就不是真正的單元測試。
            3. 易于執行 
              易于執行和生成報表。
            4. 不能超過(guò)一秒的執行時(shí)間

            單元測試的時(shí)間應該非常短,這樣就可以向開(kāi)發(fā)者快速反饋信息。這個(gè)要求其實(shí)非常的高,特別是在測試驅動(dòng)這種開(kāi)發(fā)模式中,快速高效的單元測試能夠極大的提高開(kāi)發(fā)、重構代碼的速度進(jìn)而提高和改善軟件的設計。

            還可以反過(guò)來(lái)看單元測試的規則定義,如果一個(gè)測試滿(mǎn)足下列定義的任何一個(gè),它就不是一個(gè)真正的單元測試:

            1. 訪(fǎng)問(wèn)數據庫
            2. 訪(fǎng)問(wèn)網(wǎng)絡(luò )(如 RESTful 服務(wù)接口,SOAP 服務(wù)接口的訪(fǎng)問(wèn)等等)
            3. 訪(fǎng)問(wèn)文件系統
            4. 不能獨立運行
            5. 運行單元測試需要額外的配置等

            單元測試中如果訪(fǎng)問(wèn)數據庫,網(wǎng)絡(luò ),文件系統,將會(huì )極大的影響單元測試的執行效率,執行時(shí)間一般會(huì )因 IO 操作而增加, 從而使單元測試變得太久而不可忍受,開(kāi)發(fā)人員一般希望能夠快速反饋測試結果。比如重構了代碼后第一步就是運行 單元測試,看有多少測試案例因代碼的改變而受到了影響,如果此時(shí)測試用例的運行時(shí)間過(guò)于長(cháng)久,會(huì )失去敏捷開(kāi)發(fā)的敏捷性,進(jìn)而影響開(kāi)發(fā)進(jìn)度。

            隨著(zhù)產(chǎn)品的復雜性增加,功能增加,要覆蓋更多的邏輯,單元測試代碼勢必變得更加復雜龐大,單元測試用例的簡(jiǎn)潔和獨立性就變的愈發(fā)重要,高效的單元測試代碼對開(kāi)發(fā)者提出了更高的要求。單元測試邏輯的任何對第三方的直接依賴(lài)如數據庫,網(wǎng)絡(luò ),文件系統都會(huì )降低單元測試的效率和速度。

            為滿(mǎn)足以上單元測試的要求,通過(guò)一定的方法和技巧,解脫單元測試對外界的依賴(lài)變得更有現實(shí)意義。良好的單元測試代碼會(huì )極大的改善軟件代碼的架構設計和幫助開(kāi)發(fā)人員編寫(xiě)可測試的代碼(Testable Code),提高軟件質(zhì)量。

            測試替代技術(shù)就是這樣一種方式,它可以幫助單元測試人員擺脫對第三方系統的依賴(lài),進(jìn)而提高單元測試的隔離性和執行效率。

            測試替代技術(shù)方法

            從單元測試的規則看,對單元測試的要求是很高的,特別是復雜系統,高效的單元測試案例本身,也對軟件開(kāi)發(fā)者提出了更高的要求。編寫(xiě)單元測試代碼,意味著(zhù)要求開(kāi)發(fā)者編寫(xiě)可測試的代碼,可測試的代碼隱含著(zhù)良好的代碼設計。

            隔離的單元測試意味著(zhù)把單元測試中的對第三方系統依賴(lài)的部分合理的提取出來(lái),用替代體(Test Double)取而代之,使單元測試把注意力集中放在測試“單元”的邏輯上而不是和第三方系統的交互上。

            測試替代技術(shù)的分類(lèi)

            現實(shí)開(kāi)發(fā)中,開(kāi)發(fā)人員會(huì )用不同類(lèi)型的測試替代技術(shù)去隔離測試,這些測試替代技術(shù)如圖 1 所示,一般包括:假體, 存根,模擬體和仿制體。這些類(lèi)別的測試替代技術(shù)各有自己優(yōu)點(diǎn)和缺點(diǎn)。下面將介紹每個(gè)測試替代技術(shù),并討論他們使用的范圍。

            圖 1. 測試替代技術(shù)
            圖 1. 測試替代技術(shù)

            假體 (Fake)

            假體是真正接口或抽象類(lèi)的實(shí)現體(Implementation),它是對父類(lèi)或接口的擴展和實(shí)現。假體實(shí)現了真正的邏輯,但它的存在只是為了測試,而不適合于用在產(chǎn)品中。

            比如有個(gè)簡(jiǎn)單的 Logger 類(lèi),它可以把日志寫(xiě)到文件系統或是數據庫中。下面是對應的設計類(lèi)圖。從設計可以看出 Logger 依賴(lài)于 Writer 接口,Writer 接口有兩個(gè)實(shí)現 FSWriter 和 DBWriter,分別對應著(zhù)寫(xiě)文件和寫(xiě)數據庫, 類(lèi)圖如圖 2 所示。

            圖 2. 設計類(lèi)圖
            圖 2. 設計類(lèi)圖

            Writer 通過(guò) Logger 的構造函數注入到 Logger 實(shí)例中,此時(shí)如果想測試 Logger.logFormatedMsg()單元,為了實(shí)例化 Logger,我們可以如清單 1 所示實(shí)現 Writer 的假體 FakeWriter 類(lèi),然后注入到 Logger 中去,FakeWriter 對象的 write 函數被調用時(shí) log 沒(méi)有寫(xiě)入文件系統而是保存在變量 msg 中,隔離了文件訪(fǎng)問(wèn),保存的 msg 可以用來(lái)驗證 msg 是否符合測試的期望。

            清單 1. FakeWriter 實(shí)現 Writer 接口
            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;
            }
            }

            存根(Stub)

            存根是當存根的方法被調用的時(shí)候,傳遞間接的輸入給調用者。存根的存在僅僅是為了測試。存根可以記錄一些其它的信息,如調用的次數,調用的參數等信息。比如測試中異常的處理等,忽略輸入的參數而只是拋出異常以測試單元的異常處理功能。

            清單 2. StubWriter
            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 所示,在這個(gè)測試中,StubWriter 的 write 函數忽略了輸入參數,用 Stub 只返回測試想要的測試預期,進(jìn)而測試 Logger 處理異常是不是符合期望。

            仿制體(Dummy)

            仿制體是在程序中不真實(shí)存在的對象,只是為了測試的目的而“制造”的一個(gè)虛擬對象,這個(gè)制造的仿制體對測試的邏輯幾乎沒(méi)有影響,只是為了滿(mǎn)足測試對象實(shí)例化時(shí)的依賴(lài)要求。清單 3 所示,dummyCustomer 是不真實(shí)存在的對象。

            清單 3. Dummy 測試
            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 只是為了作為數據參數滿(mǎn)足 DriverSvr 實(shí)例的 take 函數被調用,其實(shí)對我們要測試的邏輯“服務(wù)的人數”,幾乎沒(méi)有直接的影響。

            模擬體(Mock)

            模擬體本身有期望,期望是測試者賦予模擬體的。比如測試從模擬體期望一個(gè)值,在模擬體的某個(gè)方法被調用時(shí)要返回這個(gè)期望值。模擬體還可以記錄一些其他的信息,如某個(gè)函數被調用的次數等等。模擬體框架有 JMock,EasyMock,Mockito 等,他們各有特點(diǎn),但功能是相同的,都是提供模擬體以幫助測試。下面的例子用的是 Mockito,它的語(yǔ)法和語(yǔ)義使用更簡(jiǎn)單。Mockito 也可以提供存根 Stub 的功能,定在 org.mockito.stubbing 中,此處不再贅述。

            以 Mock 測試 Logger 為例,如清單 4 所示,FSWriter 的模擬體 mockedWriter 被注入到 Logger 的實(shí)例中。

            清單 4. mock 測試
            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());
            }

            測試時(shí),用 Mockito 的 ArgumentCaptor 截取要寫(xiě)入文件的信息用于驗證日志和其格式是否符合期望,從而驗證了 logger 的格式化邏輯。

            由以上的分析可以看出,模擬體 (mock) 功能最為強大和全面,是現代單元測試中最常用的一種測試輔助隔離技術(shù)。

            測試替代技術(shù)的應用

            以上主要討論的是常用的測試替代技術(shù),以下我們將討論一些在設計單元測試過(guò)程中,經(jīng)常遇到的一些需要隔離的單元測試。

            常見(jiàn)的需要隔離的訪(fǎng)問(wèn)

            要替代單元測試中的可替代體,首先讓我們來(lái)分析一下,都有哪些“第三方“需要被隔離,然后再分別有針對性的討論具體的“替代”方法。

            可明確識別的對第三方訪(fǎng)問(wèn)的有,網(wǎng)絡(luò )訪(fǎng)問(wèn),數據庫訪(fǎng)問(wèn),第三方類(lèi)庫和文件系統,下面分析一下他們各自的特點(diǎn)以及對應的可行的替代技術(shù):

            網(wǎng)絡(luò )訪(fǎng)問(wèn)

            軟件產(chǎn)品訪(fǎng)問(wèn)網(wǎng)絡(luò ),已經(jīng)變的更加普遍,隨著(zhù)現代軟件技術(shù)的發(fā)展,軟件再也不是孤立的個(gè)體,軟件產(chǎn)品需要各種網(wǎng)絡(luò )服務(wù)來(lái)滿(mǎn)足當前軟件的功能需要,無(wú)論是桌面型應用還是基于瀏覽器的 B/S 架構的軟件,幾乎不可避免的要訪(fǎng)問(wèn)網(wǎng)絡(luò )。

            特別是最近幾年的基于服務(wù)的軟件架構技術(shù)的流行,軟件程序中不得不處理 SOAP,RESTful,Socket 等等的網(wǎng)絡(luò )訪(fǎng)問(wèn)。按照單元測試的規則,單元測試中這種對網(wǎng)絡(luò )的訪(fǎng)問(wèn)應該被隔離開(kāi)來(lái),以提高測試效率。我們以 RESTFul 為例,看如何在單元測試中通過(guò)合理的設計來(lái)隔離其對網(wǎng)絡(luò )的訪(fǎng)問(wèn)。

            這個(gè)例子是用 IBM Cognos 中提供的 RESTful 的接口去提取中報表中的數據,報表中的數據可以用 GET 方式訪(fǎng)問(wèn),以 RESTful 的方式獲取,例如:

            http://HostName/ibmcognos/cgibin/cognos.cgi/rds/reportData/report/i7E932A825B08459C832B72EFC608C0FE?fmt=LDX&selection=List1

            圖 3. CognosBI 的 Report 的輸出
            圖 3. CognosBI 的 Report 的輸出

            其中 QueryString 中 LDX 定義了數據的格式,一種 XML 格式輸出,selection 選擇只取報表中的 List1 中的數據。i7E932A825B08459C832B72EFC608C0FE 是 CM 中報表的存儲 ID,可以通過(guò) CMQuery 工具獲取。LDX 的輸出中摻雜著(zhù)格式化的信息,上圖 3 是 CognosBI 的 Report 的輸出。

            此處的目標只是獲取其中的數據,所以在獲取 LDX 格式的數據后,要通過(guò) XPath 的方式抽取其中的數據部分然后轉換成另外一種可以通過(guò)行列存取的格式,數據抽取的部分邏輯和網(wǎng)絡(luò )訪(fǎng)問(wèn)定義在不同的實(shí)現類(lèi)中,他們之間的接口是抽象類(lèi) InputStream,在單元測試中數據轉換部分的測試需要隔離。相應的類(lèi)圖如圖 4 所示,DataConvert 依賴(lài)于接口 CognosClient,CognosClientService 實(shí)現了接口 CognosClient,是接口的具體實(shí)現類(lèi)。

            圖 4. 實(shí)現類(lèi)圖
            圖 4. 實(shí)現類(lèi)圖

            如程序清單 5 所示,測試中可以看到測試數據是直接嵌入到程序中,模擬體 mockedClient 被調用時(shí),直接返回了嵌入的測試數據,而沒(méi)有去訪(fǎng)問(wèn)網(wǎng)絡(luò ),實(shí)現了對網(wǎng)絡(luò )訪(fǎng)問(wèn)的隔離。(注:數據也可以以資源的方式直接嵌入到 Jar 中,然后用 this.getClass().getResourceAsStream() 加載數據,不過(guò)這似乎是間接訪(fǎng)問(wèn)了文件系統。)

            清單 5. 模擬的 CognosClient
            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;
            }

            數據庫訪(fǎng)問(wèn)

            現今數據庫訪(fǎng)問(wèn)中,特別是商業(yè)軟件,數據庫訪(fǎng)問(wèn)是系統的一部分。在軟件中,一般會(huì )通過(guò)數據庫提供的類(lèi)庫來(lái)訪(fǎng)問(wèn)數據,還用一些通用的標準如 JDBC 等提供對數據庫的訪(fǎng)問(wèn)規范和實(shí)現。軟件架構設計時(shí),數據的訪(fǎng)問(wèn)會(huì )被抽象到持久層中,在這個(gè)持久層中,會(huì )把實(shí)體和對象通過(guò) ORM 框架相互映射,如 OpenJpa 就是這樣一個(gè)框架,可以幫助開(kāi)發(fā)者很容易的實(shí)現對象和數據庫實(shí)體之間的轉換,避免了開(kāi)發(fā)者直接以寫(xiě) Sql 的方式訪(fǎng)問(wèn)數據。

            單元測試中應避免直接訪(fǎng)問(wèn)數據庫,數據庫的訪(fǎng)問(wèn)可以通過(guò)模擬體 (Mock) 對象輕松隔離開(kāi)。如我們有個(gè) UserDao 類(lèi),這個(gè)類(lèi)實(shí)現了對 User 的增刪改查,可以通過(guò) userDao=mock(UserDao.class) 和 when() 等,把所有的對通過(guò)這個(gè)類(lèi)實(shí)例訪(fǎng)問(wèn)數據庫的方法截獲并返回自己“制造”的對象或數據,從而隔離和避免了對數據庫的直接訪(fǎng)問(wèn)。

            還有些數據庫實(shí)現了內存數據庫的概念,如嵌入式數據庫 Derby,Sqlite,H2 等,單元測試中對這類(lèi)數據庫的訪(fǎng)問(wèn)利用其嵌入式接口都可以在內存完成,沒(méi)有額外的配置要求。

            文件系統

            文件系統的訪(fǎng)問(wèn),通過(guò)模擬體(Mock)的方式,可以模擬幾乎所有文件的 IO 操作,如 Logger 測試中,Writer mockedWriter = mock(FSWriter.class),在 Logger 寫(xiě)出數據到文件時(shí),寫(xiě)出的操作 write 被截獲,從而避免了對文件系統的訪(fǎng)問(wèn)。

            結束語(yǔ)

            根據單元測試的規則,單元測試中應避免對文件系統,數據庫系統,網(wǎng)絡(luò )系統的訪(fǎng)問(wèn),因為這些訪(fǎng)問(wèn)意味著(zhù)需要額外的配置(對第三方的依賴(lài)如文件路徑,數據庫鏈接,網(wǎng)絡(luò )服務(wù)器連接等等),進(jìn)而使單元測試的效率降低。假體,存根,仿制和模擬技術(shù)可以用于滿(mǎn)足這些要求,其中模擬技術(shù)功能最為全面,可以非常有效的隔離單元測試。單元測試不僅能夠提高代碼質(zhì)量,優(yōu)化代碼設計,同時(shí)也提高了開(kāi)發(fā)人員的代碼水平,節省了開(kāi)發(fā)成本,是軟件開(kāi)發(fā)過(guò)程中不可或缺的重要組成部分。

            原文轉自:https://www.ibm.com/developerworks/cn/java/j-lo-TestDoubles

            老湿亚洲永久精品ww47香蕉图片_日韩欧美中文字幕北美法律_国产AV永久无码天堂影院_久久婷婷综合色丁香五月
              <ruby id="h6500"><table id="h6500"></table></ruby>
              1. <ruby id="h6500"><video id="h6500"></video></ruby>
                    1. <progress id="h6500"><u id="h6500"><form id="h6500"></form></u></progress>