自動化單元測試實踐之路

info已經發布:
html

http://www.infoq.com/cn/articles/road-of-automated-unit-testing-practicesjava


自動化單元測試並非什麼新鮮事物,它應該是團隊鍥而不捨的事情,可能有不少團隊知道如何去作,可是還作得不夠好;還有很多團隊不知道如何去作,甚至有一些舊系統還不敢去重構,還在堅持着Java中的main方法調用的方式來執行,在漫長等待構建結果。mysql

本文主要講基於Java項目如何作自動化單元測試的實踐。web

1 是否值得spring

關於單元測試的意義,詳細參考stackoverflow這篇文章:sql

http://stackoverflow.com/questions/67299/is-unit-testing-worth-the-effort數據庫

Martin Fowler在博客(http://martinfowler.com/bliki/TestPyramid.html)中解釋了apache

TestPyramid,以下圖所示:tomcat

wps_clip_p_w_picpath-8211

圖-1-1-TestPyramid服務器

Unit是整個金字塔的基石(在建築行業,基石是作建築物基礎的石頭),若是基石不穩,Service和UI何談有構建意義呢?只有基石穩如磐石,上層建築纔夠堅固。

原本想拿瑞士作鐘錶的例子來講明下,但同事說的汽車例子更好。一輛汽車由許多配件組成,若是有如下兩種選擇,你會選擇哪一個呢?

一、全部單元配件沒有測試過,在4S店,銷售人員告訴你:剛組裝好,已經開了一天,能跑起來,你能夠試試;

二、全部單元配件在生產過程已經通過嚴格測試,在4S點,銷售人員告訴你,已經經過國家認證,出廠合格,有質量保證,你能夠試試;

答案不言而喻了。

實施單元測試,並不表明你的生產效率能提升迅猛,反而有時候阻礙了瞬間的生產效率(傳統的開發一個功能,看似就算完成的動做,增長單元測試看起來沒法是浪費時間),可是,它最直接的是提高產品質量,從而提高市場的形象,間接纔會提高生產效率。

作產品,究竟是要數量,仍是質量呢?這個應該留給老闆們去回答,看企業是否須要長遠立足。

2 關鍵部分

自動化單元測試有四個關鍵組成部分要作到統一,如圖所示:

wps_clip_p_w_picpath-23863

圖-2-1-關鍵組成部分

配置管理:使用版本控制

版本控制系統(源代碼控制管理系統)是保存文件多個版本的一種機制。通常來講,包括Subversion、Git在內的開源工具就能夠知足絕大多數團隊的需求。全部的版本控制系統都須要解決這樣一個基礎問題: 怎樣讓系統容許用戶共享信息,而不會讓他們因意外而互相干擾?

若是沒有版本控制工具的協助,在開發中咱們常常會遇到下面的一些問題:

1、 代碼管理混亂。

2、 解決代碼衝突困難。

3、 在代碼整合期間引入深層BUG。

4、 沒法對代碼的擁有者進行權限控制。

5、 項目不一樣版本發佈困難。

? 對全部內容都進行版本控制

版本控制不只僅針對源代碼,每一個與所開發的軟件相關的產物都應該被置於版本控制下,應當包括:源代碼、測試代碼、數據庫腳本、構建和部署腳本、文檔、web容器(tomcat的配置)所用的配置文件等。

? 保證頻繁提交可靠代碼到主幹

頻繁提交可靠、有質量保證的代碼(編譯經過是最基本要求),可以輕鬆回滾到最近可靠的版本,代碼提交以後可以觸發持續集成構建,及時獲得反饋。

? 提交有意義的註釋

強制要求團隊成員使用有意義註釋,甚至能夠關聯相關開發任務的緣由是:當構建失敗後,你知道是誰破壞了構建,找到可能的緣由及定位缺陷位置。這些附加信息,能夠縮短咱們修復缺陷的時間。示例:團隊使用了svn和redmine,註釋是:

refs #任務id 提交說明

每一個任務下能夠看到屢次提交記錄:

wps_clip_p_w_picpath-12436

圖-2-2-相關修訂版本

? 全部的代碼文件編碼格式統一使用UTF-8

? 上班前更新代碼,下班前提交代碼

前一天,團隊其餘成員可能提交了許多代碼到svn,開始新的一天工做是,務必更新到最新版本,及時發現問題(例如代碼衝突)並解決;

當日事,當日畢,下班別把當天的編碼成果僅保存在本地,應當提交到svn,第二天團隊更新就能夠獲取到最新版本,造成良性循環。

構建管理:使用Maven構建工具

Maven是基於項目對象模型(POM),經過爲Java項目的代碼組織結構定義描述信息來管理項目的構建、報告和文檔的軟件項目管理工具。使用「慣例勝於配置」(convention over configuration)的原則,只要項目按照Maven制定的方式進行組織,它就幾乎能用一條命令執行全部的構建、部署、測試等任務,卻不用寫不少行的XML(消除Ant文件中大量的樣板文件)。

或許,使用Ant來構建的團隊要問,爲何用Maven呢?簡單來講兩點

一、對第三方依賴庫進行統一的版本管理

說實話,ant處理依賴包之間的衝突問題,仍是得靠人工解決,這個對於研發來講是消耗時間的,倒不如把節省的時間投入到業務中去。另外不再用每一個項目繁瑣複製spring.jar了,經過maven自動管理Java庫和項目間的依賴,打包的時候會將全部jar複製到WEB- INF/lib/目錄下。

二、統一項目的目錄結構。

官方的約定:http://maven.apache.org/guides/introduction/introduction-to-the-standard-directory-layout.html

src/main/java



Application/Library sources



src/main/resources



Application/Library resources



src/main/filters



Resource filter files



src/main/config



Configuration files



src/main/scripts



Application/Library scripts



src/main/webapp



Web application sources



src/test/java



Test sources



src/test/resources



Test resources



src/test/filters



Test resource filter files



src/it



Integration Tests (primarily for plugins)



src/assembly



Assembly descriptors



src/site



Site



LICENSE.txt



Project's license



NOTICE.txt



Notices and attributions required by libraries that the project depends on



README.txt



Project's readme



保證全部項目的目錄結構在任何服務器上都是同樣的,每一個目錄起什麼做用都很清楚明瞭。

三、統一軟件構建階段

http://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html

Maven2把軟件開發的過程劃分紅了幾個經典階段,好比你先要生成一些java代碼,再把這些代碼複製到特定位置,而後編譯代碼,複製須要放到classpath下的資源,再進行單元測試,單元測試都經過了才能進行打包,發佈。

測試框架:JUnit&Mockito

? JUnit

JUnit是一個Java語言的單元測試框架。

2013年見過一箇舊項目,測試代碼仍是以main做爲入口,爲何要使用JUnit?

JUnit 的優勢是整個測試過程無人值守,開發無須在線參與和判斷最終結果是否正確,能夠很容易地一次性運行多個測試,使得開發更加關注測試邏輯的編寫,而不是增長構建維護時間。

團隊示例代碼:

// 功能代碼

package com.chinacache.portal.service;

public class ReportService {

   public boolean validateParams() {

   }

   public String sendReport(Long id) {

   }

   public String sendReport(Long id, Date time) {

   }

}

// 單元測試代碼

package com.chinacache.portal.service; // 必須與功能代碼使用相同 package

public class ReportServiceUnitTest { // 測試類名以 UnitTest (單元測試) 或 InteTest (集成測試) 結尾

   // 測試方法名以 test 開頭,而後接對應的功能方法名稱

   @Test

   public void testValidateParams() {

   }

   // 若是功能方法存在重載,則再接上參數類型

   @Test

   public void testSendReportLong() {

   }

   // 若是一個功能方法對應多個測試方法,不一樣測試方法可以使用簡潔而又有含義的單詞結尾,例如 success、fail 等    

   @Test

   public void testSendReportLongDateSuccess() {

   }

   // 這樣經過測試方法名便可知道:測的是哪一個功能方法,哪一種狀況

   @Test

   public void testSendReportLongDateFail() {

   }

}

? Mockito

Mockito是一個針對Java的mocking框架。使用它能夠寫出乾淨漂亮的測試用例和簡單的API。它與EasyMock和jMock很類似,經過在執行後校驗什麼已經被調用,消除了對指望行爲(expectations)的須要,改變其餘mocking庫以「記錄-回放」(這會致使代碼醜陋)的測試流程,使得自身的語法更像天然語言。

Mockito示例:

List mock = mock(List.class);

when(mock.get(0)).thenReturn("one");

when(mock.get(1)).thenReturn("two");

someCodeThatInteractsWithMock();

verify(mock).clear();

EasyMock示例:

List mock = createNiceMock(List.class);

expect(mock.get(0)).andStubReturn("one");

expect(mock.get(1)).andStubReturn("two");

mock.clear();

replay(mock);

someCodeThatInteractsWithMock();

verify(mock);

官方對比文章:http://code.google.com/p/mockito/wiki/MockitoVSEasyMock

反饋平臺:Jenkins&Sonar

持續集成平臺:Jenkins

Jenkins 的前身是 Hudson 是一個可擴展的持續集成引擎,主要用於:

? 持續、自動地構建測試軟件項目

? 監控一些定時執行的任務

Jenkins將做爲自動化單元測試持續集成的平臺,實現自動化構建。

wps_clip_p_w_picpath-31307

圖-2-3-Jenkins平臺

代碼質量管理平臺:Sonar

Sonar (SonarQube)是一個開源平臺,用於管理源代碼的質量。Sonar 不僅是一個質量數據報告工具,更是代碼質量管理平臺。支持的語言包括:Java、PHP、C#、C、Cobol、PL/SQL、Flex 等。

主要特色:

? 代碼覆蓋:經過單元測試,將會顯示哪行代碼被選中

? 改善編碼規則

? 搜尋編碼規則:按照名字,插件,激活級別和類別進行查詢

? 項目搜尋:按照項目的名字進行查詢

? 對比數據:比較同一張表中的任何測量的趨勢

Sonar將做爲自動化單元測試反饋報告統一展示平臺,包括:

單元測試覆蓋率、成功率、代碼註釋、代碼複雜度等度量數據的展示。

wps_clip_p_w_picpath-25900

圖-2-4 Sonar平臺

3 原則

自動化測試金字塔,也稱爲自動化分層測試,Unit是整個金字塔的基石,最重要特色是運行速度很是快;第二個重要特色是UT應覆蓋代碼庫的大部分,可以肯定一旦UT經過後,應用程序就能正常工做。

Unit:70%,大部分自動化實現,用於驗證一個單獨函數或獨立功能模塊的代碼;

Service:20%,涉及兩個或兩個以上,甚至更多模塊之間交互的集成測試;

UI:10%,覆蓋三個或以上的功能模塊,真實用戶場景和數據的驗收測試;

這裏僅僅列舉了每一個層次的百分比,實際要根據團隊的方向來作調整。

自動化單元測試原則

提交代碼、運行測試的重點是什麼?快速捕獲那些因修改向系統中引入的最多見錯誤,並通知開發人員,以便他們能快速修復他們。提交階段提供反饋的價值在於,對它的投入可讓系統高效且更快地工做。

? 隔離UI操做

UI應看成爲更高層次的測試Level,須要花費大量時間準備數據,業務邏輯複雜,過早進入UI階段,容易分散開發的單元測試精力。

? 隔離數據庫以及文件讀寫網絡開銷等操做

自動化測試中若是須要將結果寫入數據庫,而後再驗證改結果是否被正確寫入,這種驗證方法簡單、容易理解,可是它不是一個高效的方法。這個應當從集成測試的Level去解決。

首先:與數據庫的交互,是漫長的,甚至有可能要投入維護數據庫的時間,那將成爲快速測試的一個障礙,開發人員不能獲得及時有效的反饋。假設,我須要花費一個小時,才能驗證完畢與數據庫交互的結果,這種等待是多麼漫長呀。

其次,數據管理須要成本,從數據的篩選(線上數據多是T級)到測試環境的M級別,如何把篩選合適的大小,這都使得管理成本增長(固然在集成測試中可使用DBUnit來解決部分問題)。

最後,若是必定要有讀寫操做才能完成的測試,也要反思代碼的可測試性作的如何?是否須要重構。

單元測試決不要依賴於數據庫以及文件系統、網絡開銷等一切外部依賴。

? 使用Mock替身與Spring容器隔離

若是在單元測試中,還須要啓動Spring容器進行依賴注入、加載依賴的WebService等,這個過程是至關消耗時間的。

可使用模擬工具集:Mockito、EasyMock、JMock等來解決,研發團隊主要是基於Mockito的實踐。與須要組裝全部的依賴和狀態相比,使用模擬技術的測試運行起來一般是很是快,這樣子開發人員在提交代碼以後,能夠在持續集成平臺快速獲得反饋。

? 設計簡單的測試

明肯定義方法:

成功:public void testSendReportLongDateSuccess()

失敗:public void testSendReportLongDateFail(),能夠包括異常

和單一的斷言,避免在一個方法內使用多個複雜斷言,這會形成代碼結構的複雜,使得測試的複雜性提升。

? 定義測試套件的運行時間

使用Mock構建的單元測試,每一個方法的構建時間應該是毫秒級別,整個類是秒級別,理想的是總體構建時間控制在5分鐘之內,若是超過怎麼辦呢?

首先,拆分紅多個套件,在多臺機器上並行執行這些套件;

其次,重構那些運行時間比較長且不常常失敗的測試類;

更多參考推薦閱讀:《Unit Testing Guidelines》

http://geosoft.no/development/unittesting.html

4 流程

wps_clip_p_w_picpath-21033

圖-4-1-典型工做流程

一、開發人員遵循每日構建原則,提交功能代碼、測試代碼(以UnitTest結尾的測試類)到Svn;

二、Jenkins平臺,根據配置原則(假設配置定時器每6分鐘檢查Svn有代碼更新則構建)進行:代碼更新、代碼編譯、UnitTest、持續反饋的流水線工做;

三、構建結果發送到Sonar,而且把失敗的構建以郵件方式通知影響代碼的開發人員;

四、開發人員、測試人員須要在Sonar平臺進行review;

5 實踐

? Jenkins配置重點

ü 構建觸發器:推薦使用PollSCM

Poll SCM:定時檢查源碼變動(根據SCM軟件的版本號),若是有更新就執行checkout。

Build periodically:週期進行項目構建(它不care源碼是否發生變化)。

配置時間:H/6 * * * *

ü Build配置

Goals and options:emma:emma  -Dtest=*UnitTest soanr:sonar

註明:

emma:emma,Add the "emma:emma" goal to your build to generate Emma reports;

-Dtest=*UnitTest,參數配置,運行以UnitTest結尾的測試類;

sonar:sonar,來觸發靜態代碼分析。

須要安裝Emma Plugin(https://wiki.jenkins-ci.org/display/JENKINS/Emma+Plugin)

ü 構建後操做

增長Aggregate downstream test results,勾選自動整合全部的downstream測試;

增長Editable Email Notification,在「高級」選項增長觸發器「Unstable」,

勾選「Send To Committers」,Check this checkbox to send the email to anyone who  checked in code for the last build。

註明:Editable Email Notification插件是 https://wiki.jenkins-ci.org/display/JENKINS/Email-ext+plugin

另一些Jenkins的單元測試覆蓋率展示方式,能夠查看官網。

? 構建管理工具(Maven)

ü 項目統一使用Maven進行構建管理,在pom.xml中進行依賴jar包配置

ü 持續集成服務器上同時須要安裝Maven,setting.xml除了配置倉庫以外,還須要配置sonar,包括sonar服務器地址、數據庫鏈接方式:

<profile>

   <id>sonar</id>

   <activation>

   <activeByDefault>true</activeByDefault>

   </activation>

   <properties>

   <!-- EXAMPLE FOR MYSQL -->

   <sonar.jdbc.url>

     jdbc:mysql://127.0.0.1:3306/sonar?useUnicode=true&characterEncoding=utf8

   </sonar.jdbc.url>

   <sonar.jdbc.driverClassName>com.mysql.jdbc.Driver</sonar.jdbc.driverClassName>

   <sonar.jdbc.username>sonar</sonar.jdbc.username>

   <sonar.jdbc.password>sonar</sonar.jdbc.password>

   <!-- SERVER ON A REMOTE HOST -->

   <sonar.host.url>http:/127.0.0.1:9000</sonar.host.url>

   </properties>

</profile>

? Mockito配置重點

全部單元測試繼承MockitoTestContext父類

MockitoTestContext 父類:

package com.chinacache.portal;

import java.util.Locale;

import org.junit.BeforeClass;

import org.mockito.MockitoAnnotations;

import org.springframework.mock.web.MockHttpServletRequest;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import com.chinacache.portal.web.util.SessionUtil;

import com.opensymphony.xwork2.util.LocalizedTextUtil;

/**

* Mockito 測試環境。繼承該類後,Mockito 的相關注解 (@Mock, @InjectMocks, ...) 就能生效

*/

public class MockitoTestContext {    

   public MockitoTestContext() {

       MockitoAnnotations.initMocks(this);

   }

}

BillingBusinessManager 源碼:

package com.chinacache.portal.service.billing;

//引入包忽略...

/**

* 計費業務相關的業務方法

*/

@Transactional

public class BillingBusinessManager {

   private static final Log log = LogFactory.getLog(BillingBusinessManager.class);

   @Autowired

   private UserDAO userDAO;

   @Autowired

   private BillingBusinessDAO billingBusinessDAO;

   @Autowired

   private BillingBusinessSubscriptionDAO billingBusinessSubscriptionDAO;

   @Autowired

   private BillingBusinessSubscriptionDetailDAO billingBusinessSubscriptionDetailDAO;

   @Autowired

   private BillingRegionSubscriptionDAO billingRegionSubscriptionDAO;

   @Autowired

   private BillingRegionDAO billingRegionDAO;

   @Autowired

   private ContractTimeManager contractTimeManager;

   /**

    * 根據id查詢業務信息

    * @return 若是參數爲空或者查詢不到數據,返回空列表<br>

    * O 中的中、英文業務名來自 BILLING_BUSINESS 表

    */

   public List<BusinessVO> getBusinessesByIds(List<Long> businessIds) {

       return billingBusinessDAO.getBusinessbyIds(businessIds);

}

}

BillingBusinessManagerUnitTest類:

//引入包忽略...

public class BillingBusinessManagerUnitTest extends MockitoTestContext {

   @InjectMocks

   private BillingBusinessManager sv;

   @Mock

   private BillingBusinessDAO billingBusinessDAO;

   @Test

   public void testGetBusinessesByIds() {

       List<BusinessVO> expected = ListUtil.toList(new BusinessVO(1l, "a", "b"));

       //簡潔的語法以下所示

       when(billingBusinessDAO.getBusinessbyIds(anyListOf(Long.class))).thenReturn(expected);

       List<Long> businessIds = ListUtil.toList(TestConstants.BUSINESS_ID_HTTP_WEB_CACHE);

       List<BusinessVO> actual = sv.getBusinessesByIds(businessIds);

       Assert.assertEquals(expected, actual);

   }

  }

更多Mockito的使用,能夠參考官網:http://code.google.com/p/mockito/

6 總結

如何增強開發過程當中的自測環節,一直都是個頭痛的問題,開發的代碼質量究竟如何?模塊之間的質量究竟如何?迴歸測試的效率如何?重構以後,如何快速驗證模塊的有效性?

這些在沒有作自動化單元測試以前,都是難以考究的問題。惟有經過數據去衡量,橫向對比多個版本的構建分析結果,纔可以發現整個項目質量的趨勢,是提高了,仍是降低了,這樣開發、測試人員纔可以有信心作出恰當的判斷。

固然,單元測試也不是銀彈,即使項目的覆蓋率達到100%,也不能代表產品質量沒有任何問題,不會產生任何缺陷。重點在於確保單元測試環節的實施,能夠提早釋放壓力、風險、暴露問題等多個方面,改變以往沒有單元測試,全部問題都集中到最後爆發的弊端。

最後,用一張圖來作個對比:

wps_clip_p_w_picpath-12612

圖-6-1-使用先後對比

增長單元測試以後:

一、開發效率有望提高5-20%;重構、迴歸測試效率提高10%,下降出錯的概率,整體代

碼質量提高;

二、在開發過程當中暴露更多問題,將風險和壓力提早釋放,持續構建促使開發重視代碼質量;

三、UnitTest質量對於團隊來講,是可視化了,交付的是有質量的產品,而不是數量;

相關文章
相關標籤/搜索