Maven實戰(五)——自動化Web應用集成測試

自動化集成測試的角色

本專欄的上一篇文章講述了Maven與持續集成的一些關係及具體實踐,我們都知道,自動化測試是持續集成必不可少的一部分,基本上,沒有自動化測試的持續集成,都很難稱之爲真正的持續集成。我們希望持續集成能夠儘早的暴露問題,但這遠非配置一個 Hudson/Jenkins服務器那麼簡單,只有真正用心編寫了較爲完整的測試用例,並一直維護它們,持續集成才能孜孜不倦地運行測試並第一時間報告問題。

自動化測試這個話題很大,本文不想爭論測試先行還是後行,這裏強調的是測試的自動化,並基於具體的技術(Maven、 JUnit、Jetty等)來介紹一種切實可行的自動化Web應用集成測試方案。當然,自動化測試還包括單元測試、驗收測試、性能測試等,在不同的場景下,它們都能爲軟件開發帶來極大的價值。本文僅限於討論集成測試,主要是因爲筆者覺得這是一個非常重要卻常常被忽略的實踐。

基於Maven的一般流程

集成測試與單元測試最大的區別是它需要儘可能的測試整個功能及相關環境,對於測試Web應用而言,通常有這麼幾步:

  1. 啓動Web容器

  2. 部署待測試Web應用

  3. 以Web客戶端的角色運行測試用例

  4. 停止Web容器

啓動Web容器可以有很多方式,例如你可以通過Web容器提供的API採用編程的方式來啓動容器,但在Maven的環境下,配置插件顯得更簡單。如果你瞭解Maven的生命週期模型,就可能會想到,我們可以在pre-integration-test階段啓動容器,部署待測試應用,然後在integration-test階段運行集成測試用例,最後在post-integrate-test階段停止容器。也就是說,對於步驟1,2和4我們只須進行一些簡單的配置,不必編寫額外的代碼。第3步是以黑盒的形式模擬客戶端進行測試,需要注意的是,這裏通常要求你理解一些基本的HTTP協議知識,例如服務端在什麼情況下應該返回HTTP代碼 200,什麼時候應該返回401錯誤,以及所支持的Content-Type是什麼等等。

至於測試用例該怎麼寫,除了需要用到一些用來訪問Web以及解析響應詳細的基礎設施工具類之外,其他內容與單元測試大同小異,基本就是準備測試數據、訪問服務、驗證返回值等等。

一個簡單的例子

談了不少理論,現在該給個具體的例子了,譬如現在有個簡單的Servlet,它接受參數a和b,做加法後返回二者之和,如果參數不完整,則返回HTTP 400錯誤,表示客戶端的請求有問題。

public class AddServlet
    extends HttpServlet
{
    @Override
    protected void doGet( HttpServletRequest req, HttpServletResponse resp )
        throws ServletException,
            IOException
    {
        String a = req.getParameter( "a" );
        String b = req.getParameter( "b" );

        if ( a == null || b == null )
        {
            resp.setStatus( 400 );
            return;
        }

        int result = Integer.parseInt( a ) + Integer.parseInt( b );

        resp.setStatus( 200 );
        resp.getWriter().print( result );
    }
}

爲了測試這段代碼,我們需要一個Web容器,這裏暫且使用Jetty,因爲目前來說它與Maven集成的相對最好。Jetty提供了一個Jetty Maven Plugin,藉助該插件,我們可以隨時啓動Jetty並部署Maven默認目錄佈局的Web項目,實現快速開發和測試。這裏我們需要的是在pre-integration-test階段啓動Jetty,在post-integrate-test階段停止容器,對應的POM配置如下:

      <plugin>
        <groupId>org.mortbay.jetty</groupId>
        <artifactId>jetty-maven-plugin</artifactId>
        <version>7.3.0.v20110203</version>
        <configuration>
          <stopPort>9966</stopPort>
          <stopKey>stop-jetty-for-it</stopKey>
        </configuration>
        <executions>
          <execution>
            <id>start-jetty</id>
            <phase>pre-integration-test</phase>
            <goals>
              <goal>run</goal>
            </goals>
            <configuration>
              <daemon>true</daemon>
            </configuration>
          </execution>
          <execution>
            <id>stop-jetty</id>
            <phase>post-integration-test</phase>
            <goals>
              <goal>stop</goal>
            </goals>
          </execution>
        </executions>
      </plugin>

XML代碼中第一處configuration是插件的全局配置,stopPort和 stopKey是該插件用來停止Jetty需要用到的TCP端口及消息關鍵字。接着是兩個executation元素,第一個executation將 jetty-maven-plugin的run目標綁定至Maven的pre-integration-test生命週期階段,表示啓動容器,第二個 executation將stop目標綁定至post-integration-test生命週期階段,表示停止容器。需要注意的是,啓動Jetty時我們需要配置deamon爲true,讓Jetty在後臺運行以免阻塞mvn命令。此外,jetty-maven-plugin的run目標也會自動部署當前Web項目。

準備好Web容器環境之後,我們接着看一下測試用例代碼:

public class AddServletIT
{
    @Test
    public void addWithParametersAndSucceed()
        throws Exception
    {
        HttpClient httpclient = new DefaultHttpClient();
        HttpGet httpGet = new HttpGet( "http://localhost:8080/add?a=1&b=2" );
        HttpResponse response = httpclient.execute( httpGet );

        Assert.assertEquals( 200, response.getStatusLine().getStatusCode() );
        Assert.assertEquals( "3", EntityUtils.toString( response.getEntity() ) );
    }

    @Test
    public void addWithoutParameterAndFail()
        throws Exception
    {
        HttpClient httpclient = new DefaultHttpClient();
        HttpGet httpGet = new HttpGet( "http://localhost:8080/add" );
        HttpResponse response = httpclient.execute( httpGet );

        Assert.assertEquals( 400, response.getStatusLine().getStatusCode() );
    }
}

爲了能夠訪問應用,這裏用到了HttpClient,兩個測試方法都初始化一個HttpClient,然後創建HttpGet對象用來訪問Web地址。第一個測試方法顧名思義用來測試成功的場景,它提供參數 a=1和b=2,執行請求後,驗證返回結果成功(HTTP狀態碼200)並且內容爲正確的值3。第二個測試方法則用來測試失敗的場景,當不提供參數的時候,服務器應該返回一個HTTP 400錯誤。該測試類其實是相當粗糙的,例如有硬編碼的服務器URL,這裏的目的僅僅是通過儘可能簡單的代碼來展現一個自動化集成測試的實現過程。

上述代碼中,測試類的名稱爲AddServletIT,而不是一般的**Test,IT表示IntegrationTest,這麼命名是爲了和單元測試區分開來,這樣,鑑於Maven默認的測試命名約定,Maven在test生命週期階段執行單元測試時,就不會涉及集成測試。現在,我們希望Maven在integration-test階段執行所有以IT結尾命名的測試類,配置Maven Surefire Plugin如下:

      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-surefire-plugin</artifactId>
        <version>2.7.2</version>
        <executions>
          <execution>
            <id>run-integration-test</id>
            <phase>integration-test</phase>
            <goals>
              <goal>test</goal>
            </goals>
            <configuration>
              <includes>
                <include>**/*IT.java</include>
              </includes>
            </configuration>
          </execution>
        </executions>
      </plugin>

通過命名規則和插件配置,我們優雅地分離了單元測試和集成測試,而且我們知道在integration-test階段,Jetty容器已經啓動完成了。如果你在使用TestNG,那你還可以使用其測試組的特性來分離單元測試和集成測試,Maven Surefire Plugin對其也有着很好的支持

一切就緒了,運行 mvn clean install 以自動運行集成測試,我們可以看到如下的輸出片段:

[INFO] --- jetty-maven-plugin:7.3.0.v20110203:run (start-jetty) @ webapp-demo ---
[INFO] Configuring Jetty for project: webapp-demo
[INFO] webAppSourceDirectory /home/juven/git_juven/webapp-demo/src/main/webapp does not exist. Defaulting to /home/juven/git_juven/webapp-demo/src/main/webapp
[INFO] Reload Mechanic: automatic
[INFO] Classes = /home/juven/git_juven/webapp-demo/target/classes
[INFO] Context path = /
...
2011-03-06 14:55:15.676:INFO::Started [email protected]:8080
[INFO] Started Jetty Server
[INFO] 
[INFO] --- maven-surefire-plugin:2.7.2:test (run-integration-test) @ webapp-demo ---
[INFO] Surefire report directory: /home/juven/git_juven/webapp-demo/target/surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.juvenxu.webapp.demo.AddServletIT
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.344 sec

Results :

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0

[INFO] 
[INFO] --- jetty-maven-plugin:7.3.0.v20110203:stop (stop-jetty) @ webapp-demo ---

可以看到jetty-maven-plugin:7.3.0.v20110203:run對應了start-jetty,maven-surefire- plugin:2.7.2:test對應了run-integration-test,jetty-maven- plugin:7.3.0.v20110203:stop對應了stop-jetty,與我們的配置和期望完全一致。此外兩個測試也都成功了!

小結

相對於單元測試來說,集成測試更難編寫,因爲需要準備更多的環境,本文只涉及了Web容器最簡單的情形,實際的開發情形中,你可能會遇到數據庫,第三方Web服務,更復雜的容器配置和數據格式等等,這都使得編寫集成測試變得讓人畏懼。然而反過來考慮,無論如何你都需要測試,雖然這個自動化過程的投入很大,但收益往往更加客觀,這不僅僅是手動測試時間的節省,更重要的是,你無法保證手動測試能被高頻率的反覆執行,也就無法保證問題能被儘早暴露。

對於Web應用來說,編寫集成測試有助於你考慮和設計Web應用對外暴露的接口,這種“開發實現”/“測試審察”之間的角色轉換往往能造就更清晰的設計,這也是編寫測試最大的好處之一。

Maven用戶能夠得益於Maven的插件系統,不僅能節省大量的編碼,還能得到穩定的工具,Jetty Maven Plugin和Maven Surefire Plugin就是最好的例子。本文只涉及了Jetty,如果讀者的環境是Tomcat或者JBoss等其他容器,則需要查閱相關的文檔以得到具體的實現細節,你可能對Tomcat Maven PluginJBoss Maven Plugin、或者Cargo Maven2 Plugin感興趣。


原文地址:http://www.infoq.com/cn/news/2011/03/xxb-maven-5-integration-test

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章