自动化集成测试的角色

本专栏的上一篇文章讲述了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( );
return;
} int result = Integer.parseInt( a ) + Integer.parseInt( b ); resp.setStatus( );
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..v20110203</version>
<configuration>
<stopPort></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( , 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( , 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.</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..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 = /
...
-- ::15.676:INFO::Started SelectChannelConnector@0.0.0.0:
[INFO] Started Jetty Server
[INFO]
[INFO] --- maven-surefire-plugin:2.7.: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: , Failures: , Errors: , Skipped: , Time elapsed: 0.344 sec Results : Tests run: , Failures: , Errors: , Skipped: [INFO]
[INFO] --- jetty-maven-plugin:7.3..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感兴趣。

关于作者

许晓斌(Juven Xu),国内社区公认的Maven技术专家、Maven中文用户组创始人、Maven技术的先驱和积极推动者。对Maven有深刻的认识,实战经验丰富,不仅撰写了大量关于Maven的技术文章,而且还翻译了开源书籍《Maven权威指南》,对Maven技术在国内的普及和发展做出了很大的贡献。就职于Maven之父的公司,负责维护Maven中央仓库,是Maven仓库管理器Nexus(著名开源软件)的核心开发者之一,曾多次受邀到淘宝等大型企业开展Maven方面的培训。此外,他还是开源技术的积极倡导者和推动者,擅长Java开发和敏捷开发实践。他的个人网站是:http://www.juvenxu.com

来源:http://www.infoq.com/cn/news/2011/03/xxb-maven-5-integration-test

学习笔记——Maven实战(五)自动化Web应用集成测试的更多相关文章

  1. 学习笔记——Maven实战(六)Gradle,构建工具的未来?

    Maven面临的挑战 软件行业新旧交替的速度之快往往令人咂舌,不用多少时间,你就会发现曾经大红大紫的技术已经成为了昨日黄花,当然,Maven也不会例外.虽然目前它基本上是Java构建的事实标准,但我们 ...

  2. 学习笔记——Maven实战(八)常用Maven插件介绍(下)

    我们都知道Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由maven- compiler-plugin完成的.进一步说,每个任务对应 ...

  3. 学习笔记——Maven实战(四)基于Maven的持续集成实践

    Martin的<持续集成> 相信很多读者和我一样,最早接触到持续集成的概念是来自Martin的著名文章<持续集成>,该文最早发布于2000年9月,之后在2006年进行了一次修订 ...

  4. 学习笔记——Maven实战(七)常用Maven插件介绍(上)

    我们都知道Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由maven-compiler-plugin完成的.进一步说,每个任务对应了 ...

  5. 学习笔记-[Maven实战]-第三章:Maven使用入门(1)

    说明:[Maven实战]一书还介绍了怎么样手工创建Maven工程,学习这本书是为了能尽快在工作中使用,就忽略了手工建工程的部分 如果想了解这部分的内容,可以自己看看书 开始: 1.新建一个maven工 ...

  6. 学习笔记——Maven实战(一)坐标规划

    坐标是什么?为什么要规划? 坐标是Maven最基本的概念,它就像每个构件的身份证号码,有了它我们就可以在数以千万计的构件中定位任何一个我们感兴趣的构件.举个最简单的例子,如果没有坐标,使用JUnit的 ...

  7. 学习笔记——Maven实战(二)POM重构之增还是删

    重构是广大开发者再熟悉不过的技术,在Martin Fowler的<重构——改善既有代码的设计>一书中,其定义为“重构(名词):对软件内部结构的一种调整,目的是在不改变软件之可察行为前提下, ...

  8. 学习笔记——Maven实战(三)多模块项目的POM重构

    重复,还是重复 程序员应该有狗一般的嗅觉,要能嗅到重复这一最常见的坏味道,不管重复披着怎样的外衣,一旦发现,都应该毫不留情地彻底地将其干掉.不要因为POM不是产品代码而纵容重复在这里发酵,例如这样一段 ...

  9. 学习笔记——Maven实战(九)打包的技巧

    “打包“这个词听起来比较土,比较正式的说法应该是”构建项目软件包“,具体说就是将项目中的各种文件,比如源代码.编译生成的字节码.配置文件.文档,按照规范的格式生成归档,最常见的当然就是JAR包和WAR ...

随机推荐

  1. 微信开发(03)之新建按钮时报错 errcode 40054

    在微信开发新建公众号的按钮时,报错如下: {errcode:40054,errmsg:"invalid sub button url domain"} 经过仔细排查,发现是url地 ...

  2. 第四篇 :微信公众平台开发实战Java版之完成消息接受与相应以及消息的处理

    温馨提示: 这篇文章是依赖前几篇的文章的. 第一篇:微信公众平台开发实战之了解微信公众平台基础知识以及资料准备 第二篇 :微信公众平台开发实战之开启开发者模式,接入微信公众平台开发 第三篇 :微信公众 ...

  3. IE6/7/8不支持jQuery创建非闭合格式的链接A

    代码如下 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <scri ...

  4. JavaScript中点号“.”的多义性

    点号「.」在JavaScript中有两种语义 语义1.表示算术中的小数点(浮点数),如 2.5 语义2.取对象属性.方法,如 [].push(2) 这几乎没有任何难理解的地方,但下面这个问题则很有趣. ...

  5. linux—【用户和组的管理操作】(5)

    用户:user 组:group 增加:add 修改:modify   mod 删除:delete    del useradd     增加用户 usermod    修改用户 userdel     ...

  6. [转]ionic Accordion list three levels

    简化后的主要代码: $scope.groups = []; for (var i = 0; i < 2; i++) { $scope.groups[i] = { name: i, items: ...

  7. Qt与VC编程合作起龌龊

    由于历史原因,某软件项目的界面采用QT,而后台用了VC,界面静态调用了VC生成的dll,一直以来都能够快乐的合作,然而最近出现两个小问题,觉得两者之间的合作并没有想象的那么美好. 在VC下用多媒体定时 ...

  8. bzoj-2748 2748: [HAOI2012]音量调节(dp)

    题目链接: 2748: [HAOI2012]音量调节 Time Limit: 3 Sec  Memory Limit: 128 MB Description 一个吉他手准备参加一场演出.他不喜欢在演出 ...

  9. poj1459 Power Network (多源多汇最大流)

    Description A power network consists of nodes (power stations, consumers and dispatchers) connected ...

  10. Python天猫淘宝评论爬虫

    说明 由于Github 打包的exe某些文件上传被.gitignore了,所以不提供windows二进制包 https://github.com/hunterhug/taobaocomment 一个抓 ...