如何有效地进行集成测试一直是困扰Java程序员的问题,由于在开发中,有很多代码都是需要运行在各种容器当中的,因此有的时候我们不得不去手工管理这些容器的生命周期。针对这些问题,Java社区提出了很多解决方案:比如Maven提供Jetty插件用于执行集成测试,Spring也在JUnit测试中提供Spring Bean的容器支持。而Arquillian则是JBoss社区针对这一问题提供给我们的解决方案,让我们在基于JBoss AS的环境下开发时,可以很容易地撰写测试代码。
Arquillian的设计目标包括:控制JBoss AS的启动与关停;在JBoss AS7中部署测试代码;将测试代码打包成WAR,EAR,JAR等多种形式;支持CDI、Session Bean等各种J2EE的标准Bean;与JUnit的集成 --- 用一句话来概括,Arquillian让我们专注于撰写测试代码本身,而不需要关注其它的配置,部署,启动JBoss AS等工作。
接下来我们可以通过一个实际的例子来看下Arquillian的使用方法,因为Arquillian是JBoss社区为基于JBoss AS的开发环境量身定做的测试工具,因此本文中的例子将结合JBoss AS7的环境来讲解Arquillian,为此,我们首先要准备好JBoss AS7。
准备JBoss AS7
我们之所以要取出JBoss AS7的源代码,而不是下载已经编译好的版本,具体原因有两个:一是Arquillian需要用到JBoss AS7源代码中所包含的一个组件;二是我们稍后会引用到JBoss AS7中的一些代码进行说明。当然,我们至少可以借此机会熟悉一下JBoss AS7的编译过程。
首先取出JBoss AS7的源代码,AS7的源代码位于github上,要想签出代码,首先要确保你的机器上装了git,git安装不是本文重点,在此不展开讲述。下面是签出JBoss AS7源代码的git命令:
git clone https://github.com/jbossas/jboss-as.git
耐心等待代码下载完成后,进到jboss-as代码目录,执行编译命令:
./build.sh
JBoss AS7通过调用代码中自带的Maven 3进行编译,如果仔细查看build.sh的内容,会发现它调用的是tools目录中的maven:
liweinan@smart:~/projs/jboss-as$ ls tools/
maven
整个编译的时间会比较漫长,Maven需要从网上下载AS7所需的包,在我的机器上编译AS7大概需要2小时。编译完成后,我们便得到了一个可运行的JBoss AS7服务器,位于build/target目录当中:
liweinan@smart:~/projs/jboss-as$ ls build/target/
antrun site
jboss-as-7.1.0.Alpha1-SNAPSHOT
其中jboss-as-7.1.0.Alpha1-SNAPSHOT就是编译好的JBoss AS服务器,因为我们签出的是JBoss AS7的trunk code,因此随着时间的推移,具体的版本会有变化,我在签出时最新的版本就是上面所见的7.1.0.Alpha1-SNAPSHOT
Arquillian等下要用到这个编译好的JBoss AS服务器来执行测试,目前JBoss AS7的准备工作就算是做完了。接下来我们来撰写测试代码。
撰写代码实例
首先使用Maven来创建一个项目:
mvn archetype:create -DarchetypeGroupId=org.apache.maven.archetypes -DgroupId=net.bluedash -DartifactId=try-aqua
接下来是在src/test/java中撰写测试代码,这是Maven生成的项目中放置单元测试代码的标准位置。我们做一个摄氏华氏温度转换工具[2] :
package net.bluedash;
public class TemperatureConverter {
public double convertToCelsius(double f) {
return ((f - 32) * 5 / 9);
}
public double convertToFarenheit(double c) {
return ((c * 9 / 5) + 32);
}
}
接下来我们来撰写单元测试代码:
package net.bluedash;
import junit.framework.TestCase;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchivePaths;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.asset.ByteArrayAsset;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.inject.Inject;
@RunWith(Arquillian.class)
public class AppTest extends TestCase {
@Inject
private TemperatureConverter converter;
@Deployment
public static Archive<?> deployment() {
return ShrinkWrap.create(JavaArchive.class, "test.jar")
.addClasses(TemperatureConverter.class)
.addAsManifestResource(
new ByteArrayAsset("<beans/>".getBytes()),
ArchivePaths.create("beans.xml"));
}
@Test
public void testConvertToCelsius() {
assertEquals(converter.convertToCelsius(32d), 0d);
assertEquals(converter.convertToCelsius(212d), 100d);
}
@Test
public void testConvertToFarenheit() {
assertEquals(converter.convertToFarenheit(0d), 32d);
assertEquals(converter.convertToFarenheit(100d), 212d);
}
}
上面的代码有几点需要说明:
@RunWith(Arquillian.class)
上面这行代码让Arquillian来接管测试的执行过程,包括后续的JBoss AS启动与,在AS中部署项目,执行测试,以及测试完成后卸载测试项目等工作。我们接着看后面的代码:
@Inject
private TemperatureConverter converter;
我们使用了CDI的标记类javax.inject.Inject来注入我们的待测试组件TemperatureConverter,这个标记是要在J2EE容器中运行方能生效的,在普通的单元测试下面并不能发挥作用,正因如此,我们这个单元测试要部署到JBoss AS服务器中方能执行。
接下来是:
@Deployment
public static Archive<?> deployment() {
return ShrinkWrap.create(JavaArchive.class, "test.jar")
.addClasses(TemperatureConverter.class)
.addAsManifestResource(
new ByteArrayAsset("<beans/>".getBytes()),
ArchivePaths.create("beans.xml"));
}
上面这段代码是使用Arquillian的核心所在:我们使用ShrinkWrap这个类来将本单元测试打包成jar,生成test.jar,并在这个jar中添加本单元测试的类TemperatureConverter.class。这样,Arquillian便可以将我们的项目打包成jar并在测试时部署进JBoss AS。
下面这张结构图可以帮我们理解Arquillian在系统中所处的位置[3] :
单元测试的代码需要讲解的就是这些,但是要想让测试顺利执行,Arquillian还需要一个配置文件,我们接下来仔细看下这个配置文件。
arquillian.xml
我们将这个文件按maven的项目目录结构要求,放在resources目录下面:
try-aqua/src/test$ mkdir resources
try-aqua/src/test$ ls
java resources
在resources目录下面创建配置文件arquillian.xml:
<?xml version="1.0" encoding="UTF-8"?>
<arquillian xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://jboss.org/schema/arquillian"
xsi:schemaLocation="http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd">
<container qualifier="jboss7" default="true">
<configuration>
<property name="jbossHome">/Users/liweinan/projs/jboss-as/build/target/jboss-as-7.1.0.Alpha1-SNAPSHOT</property>
<property name="modulePath">/Users/liweinan/projs/jboss-as/build/target/jboss-as-7.1.0.Alpha1-SNAPSHOT/modules</property>
</configuration>
</container>
</arquillian>
在这个配置文件中,我们指定了两个参数,分别是jbossHome及modulePath。我们要将这两个参数指向之前编译好的 JBoss AS7所在的目录,这样Arquillian才能找到JBoss AS并正确执行单元测试。
pom.xml
最后是项目的装配工作,在maven的配置文件pom.xml中,有下述几点配置值得说明:
<repository>
<id>JBossMavenRepo</id>
<name>JBoss Maven2 repo</name>
<url>https://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
我们让maven使用jboss提供的maven仓库,从而下载所需的Arquillian组件:
<properties>
<arquillian.version>1.0.0.CR4</arquillian.version>
</properties>
...
<dependency>
<groupId>org.jboss.arquillian.junit</groupId>
<artifactId>arquillian-junit-container</artifactId>
<version>${arquillian.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.container</groupId>
<artifactId>arquillian-container-test-api</artifactId>
<version>${arquillian.version}</version>
<scope>test</scope>
</dependency>
在dependency中,我们指定所需的Arquillian组件。接下来是JUnit:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
这里要注意的是,要使用JUnit 4,不可以使用3.x或更低的版本,因为我们的单元测试用到了JUnit 4里面的一些标记类。最后一个依赖需要特别说明:
<dependency>
<groupId>org.jboss.as</groupId>
<artifactId>jboss-as-arquillian-container-managed</artifactId>
<version>7.1.0.Alpha1-SNAPSHOT</version>
<scope>test</scope>
</dependency>
jboss-as-arquillian-container-managed是Arquillian与JBoss AS7交互的重要组件,这个组件的版本必须要和测试所使用的JBoss AS7版本一致,否则可能会导致测试无法正常运行!这也是为什么我们要从源代码编译JBoss AS7,因为在JBoss AS7的源代码中包含这个组件,因此,只要JBoss AS7在本地编译通过了,这个组件便会安装到maven的本地仓库中,并保证和编译出来的JBoss AS7版本一致。
我在撰写此文时,JBoss AS7的代码版本为7.1.0.Alpha1-SNAPSHOT,因此在上面的dependency配置中进行了指定。总之最终目的是保证这个组件的版本与arquillian.xml中指定的JBoss AS7版本要一致。
这样,我们的例子便完成了,接下来可以执行测试看看过程及结果。
执行测试
使用maven命令:
mvn test
系统输出如下:
liweinan@smart:~/projs/bl/try-aqua$ mvn test
[INFO] Scanning for projects...
...
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running net.bluedash.AppTest
Sep 22, 2011 3:00:10 AM org.jboss.arquillian.container.impl.client.container.ContainerRegistryCreator getActivatedConfiguration
INFO: Could not read active container configuration: null
Sep 22, 2011 3:00:10 AM org.jboss.as.arquillian.container.managed.ManagedDeployableContainer startInternal
...
03:00:11,616 INFO [org.jboss.as] JBoss AS 7.1.0.Alpha1-SNAPSHOT "Lightning" starting
...
03:00:18,876 INFO [org.jboss.as.server.deployment] (MSC service thread 1-4) Starting deployment of "test.jar"
...
03:00:19,773 INFO [org.jboss.as.server.deployment] (MSC service thread 1-4) Stopped deployment test.jar in 19ms
...
Results :
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
我将重要的日志输出列在了上面,可以看到Arquillian将JBoss AS7启动了起来,将测试代码打包成test.jar并进行了部署,执行测试完成后,退出了JBoss AS7。
小结
我们通过一个简单的例子学习了Arquillian在JBoss AS7的使用方法,在实际使用中,Arquillian还可以帮助我们完成更多的测试任务,比如针对WEB项目将测试代码打包成WAR或者EAR。如果你想对Arquillian的使用有深入的学习了解,可以参考Arquillian的官方文档[2] 。
如果想看更多实际使用Arquillian进行JBoss AS下测试的实例,JBoss AS7自身的源代码无疑是最好的学习资料:在JBoss AS7的源代码目录中,有一个名为testsuite的目录,这里面有一个integration目录,里面有大量针对JBoss AS7各组件的集成测试,全部使用Arquillian进行测试代码的编写,是很好的学习资料。
样例代码
本文中所用到的样例代码,我放到了github上面,有兴趣的朋友可以自己下载玩玩看:
https://github.com/liweinan/try-aqua
注释
[1] https://docs.jboss.org/author/display/ARQ/JBoss+AS+7.0+-+Managed
Arquillian的JBoss AS7受控方式配置说明:Arquillian通过DeployableContainer来控制JBoss AS 7服务的启动,加载单元测试,执行测试及测试结束后服务的关停。本文主要是使用了Arquillian的这种工作方式。
[2] https://docs.jboss.org/author/display/ARQ/Writing+your+first+Arquillian+test
本文的测试代码来自于这篇文档。
[3] http://docs.jboss.org/arquillian/reference/latest/en-US/html/intro.html#overview
文章中使用的Arquillian结构图来源于此。