我想分享我在发布此问题后学到的知识,并将我的第一个答案放在 StackExchange(我无数次通过谷歌访问该网站以寻找解决我无尽问题的解决方案)
单元 vs 集成 vs 功能测试连续体
关于这个问题有很多纠正、争论和拖钓,所以我想澄清一下。这一切真的很简单。假设你有一些服务。当您调用它时,会出现一系列事件,我将简单地说明为:
(收到请求)-(调用函数 1)-(调用函数 2)-(调用函数 3)-(发送响应)
单元测试单独测试每个函数(或类或单元),输入输入并检查输出。集成测试需要几个单元(例如功能 2-功能 3 链),并且还进行 ol' in-and-out。功能测试贯穿整个链条,从请求到响应。我将让读者猜测在每个规模级别进行测试的一些优点和缺点。无论如何,所有这些测试都可以在服务器中运行,并且有充分的理由在那里运行它们。
容器内/服务器内测试的类型
-
Container-in-the-tests Spring 和其他依赖注入框架的一项功能允许您为每个测试设置一个仅填充最少的类(加上所有模拟)的容器。这非常方便,因为它消除了手动布线的需要,并且更好地接近了生产环境。这仅允许单元和集成测试。
- 优点:
a) 传统的单元测试(具有集中测试和隔离测试的优点)变得更加方便
b)更接近生产环境,因为您正在测试自动装配逻辑
e) 与 IDE 测试运行器集成
f) 快速
- 缺点:
a) 环境可能与生产环境有很大不同
b) 不能取代功能测试的需要
-
Server-in-the-tests 一个普通的测试运行器运行几乎普通的单元测试,它启动一个嵌入式服务器或容器,并对其进行调用。一些框架(如 Jersey Testing Framework)只允许功能测试,但大多数(Arquillian、jeeunit)允许你做所有类型的测试。使用其中一些框架,就好像测试在您的代码旁边运行在服务器上,并且可以进行任何类型的调用。
- 优点(除了您可以访问所有容器和服务器服务的事实):
a) 你有独立的测试,不需要安装或设置任何东西
b) 测试是隔离的,因为为每个测试或测试套件创建了一个新的服务器/容器。
b) 与 IDE 测试运行器集成
- 缺点:
a) 环境可能与生产环境有很大不同(例如,Jetty 不是 Tomcat 或 Glassfish)
b)启动/停止服务器会减慢测试速度 c)框架很糟糕。 Jeeunit 是一个小项目,甚至还没有在 Windows 上进行过测试,Arquillian 很大但非常新,文档记录很差,我也无法让它工作。
-
Tests-in-the-server 在这里,测试实际上是与您的代码一起编译并与您的代码一起运行的。
- 优点:
a)您有简单的旧测试,不需要了解或使用任何类型的框架
- 缺点:
a) 测试之间没有隔离(不一定是问题,甚至是劣势,但可能必须采取预防措施)
b) 不与 IDE 测试运行程序集成(至少在 Netbeans 中)
-
在构建过程中使用 Maven Maven 启动一个服务器,加载您的特殊测试 WAR,执行测试,并提供一个不错的 Surefire 报告。
- 其他优势:
a)它在构建期间完成(并将与持续集成工具和其他工具集成)
b) 无需安装或设置任何东西(Maven 会自动下载、运行等服务器)
- 其他缺点:
a) 环境可能完全不同(Maven 使用 Jetty,它在您的机器上运行)
b) 无法在生产环境中重新运行
-
in-WAR 测试 测试是使用您的代码永久编译的。无论何时何地您的 WAR 启动,您都可以启动测试。在您的开发服务器上,在暂存期间,甚至在生产中。这就是我最初的问题。
- 其他优势:
a) 完全正确的环境。
b) 随时运行测试
- 其他缺点:
a) 需要设置服务器
还有一点需要说明。 Netbeans 将 Maven 测试的大部分优势提供给了 WAR 内测试。它包括一个嵌入式服务器,并在构建后自动启动并部署到它。它甚至可以打开 Firefox ......只需将其设置为指向您的 /test 资源。这就像用 Maven 方式做的,但更好。
无论如何,我将向您展示如何在同一个 Maven 项目中同时进行 Maven 测试和 WAR 内测试。
使用 Spring 的容器测试:
Spring 是一个庞大的容器框架。它的依赖注入机制与 Jax-RS 交织在一起,取得了辉煌的效果,但代价是显着的学习曲线。我不会解释 Spring 或 Jax-RS 是如何工作的。我将直接进入说明,希望读者可以将这些想法应用于其他场景。
让容器进入 JUnit 4 测试的方法是使用 Spring 测试运行器,声明你想在容器中注册的类,注册一些 Jax-RS 特定的帮助类,注册你的模拟,最后像使用普通类一样使用您的 Jax-RS 资源:
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(classes={
MyClass1.class,
Myclass2.class,
MyJaxRsResource.class,
MockServletContextAwareProcessor.class,
MyCTest.Config.class
})
public class MyCTest
{
@Configuration
static class Config
{
// Set up and register mocks here, and watch them be autowired!
@Bean public DBService dbJobService() throws DBException
{
return mock(DBService.class);
}
}
@Autowired MyJaxRsResource myResource;
@Test public void test() {
String response = myResource.get("hello");
}
}
@WebAppConfiguration 注入自己的 ServletContextAwareProcessor。但是,当必须动态设置解压 WAR 文件的路径时,MockServletContextAwareProcessor 是必需的,因为 WebAppConfiguration 只允许您在编译时静态设置路径。在运行服务器中的测试时使用这个类(见下文),我注入了真正的 ServletContext。我使用 Spring 的配置文件功能通过环境变量来抑制它(这不是很优雅)。 setServletContext 仅由服务器测试运行程序调用。
@Configuration
public class MockServletContextAwareProcessor {
public static void setServletContext(ServletContext sc) {
servletContext = sc;
}
private static ServletContext getServletContext() {
return servletContext;
}
private static ServletContext servletContext;
@Configuration
@Profile("server-test")
static class ServerTestContext {
static public @Bean
ServletContextAwareProcessor
scap() {
ServletContext sc = getServletContext();
return new ServletContextAwareProcessor(sc);
}
}
}
使用 Maven 的服务器在测试:
第 1 步)在 /src/test 文件夹中创建常规 JUnit 测试,但将它们命名为 IT*.java 或 *IT.java 或 *ITCase.java(例如,MyClassIT.java) 您可以将它们命名为不同的名称,但这是 Failsafe 默认情况下所期望的。 IT 代表集成测试,但测试代码可以位于测试连续体的任何位置。例如,您可以实例化一个类并对其进行单元测试,或者您可以启动 HttpClient(或 Jersey 客户端),将其指向您自己(注意下面的端口),然后在功能上测试您的入口点。
public class CrossdomainPolicyResourceSTest extends BaseTestClass {
static com.sun.jersey.api.client.Client client;
@BeforeClass public static void
startClient() {
client = Client.create();
}
@Test public void
getPolicy() {
String response =
client
.resource("http://localhost/crossdomain.xml")
.get(String.class);
assertTrue(response.startsWith("<?xml version=\"1.0\"?>"));
}
}
BaseTestClass 只是一个小助手类,它打印测试类的名称并在执行时进行测试(对服务器中的测试很有用,见下文):
public abstract class BaseTestClass {
@ClassRule public static TestClassName className = new TestClassName();
@Rule public TestName testName = new TestName();
@BeforeClass public static void
printClassName() {
System.out.println("--" + className.getClassName() + "--");
}
@Before public void
printMethodName() {
System.out.print(" " + testName.getMethodName());
}
@After public void
printNewLine() {
System.out.println();
}
}
第 2 步)将 maven-failsafe-plugin 和 maven-jetty-plugin 添加到您的 pom.xml 中
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.11</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.mortbay.jetty</groupId>
<artifactId>maven-jetty-plugin</artifactId>
<version>6.1.26</version>
<configuration>
<!-- By default the artifactId is taken, override it with something simple -->
<contextPath>/</contextPath>
<scanIntervalSeconds>2</scanIntervalSeconds>
<stopKey>foo</stopKey>
<stopPort>9999</stopPort>
<connectors>
<connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
<port>9095</port>
<maxIdleTime>60000</maxIdleTime>
</connector>
</connectors>
</configuration>
<executions>
<execution>
<id>start-jetty</id>
<phase>pre-integration-test</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<scanIntervalSeconds>0</scanIntervalSeconds>
<daemon>true</daemon>
</configuration>
</execution>
<execution>
<id>stop-jetty</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
步骤 3) 利润。真的,就是这样!只需在 IDE 中运行 'mvn install' 或点击 build,代码将构建,您的常规 *Test.java 测试将运行,码头服务器将启动,*IT.java 测试将运行,您将获得一份不错的报告。
将您的测试打包到您的 WAR 中以在任何地方运行:
(与上述说明一起使用或单独使用)
第 1 步)通过指示 maven-war-plugin 将测试类(src/test/ 目录)嵌入到 WAR 中:(改编自 here)
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.1.1</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<webResources>
<resource>
<directory>${project.build.directory}/test-classes</directory>
<targetPath>WEB-INF/classes</targetPath>
</resource>
<resource>
<directory>${project.build.directory}/test-libs</directory>
<targetPath>WEB-INF/lib</targetPath>
</resource>
</webResources>
</configuration>
</plugin>
注意:您可以通过在其配置集中创建额外的执行和(我留给读者的详细信息)来创建一个带有集成测试的单独 WAR
注意:理想情况下,以上内容将排除所有常规测试(并且仅复制 *IT.java)但是,我无法让包含/排除工作。
您还必须通过为 maven-dependency-plugin 提供额外的执行来包含测试库,目标是包含测试范围的复制依赖项
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.1</version>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<excludeScope>compile</excludeScope>
<outputDirectory>${project.build.directory}/test-libs</outputDirectory>
<overWriteReleases>true</overWriteReleases>
<overWriteSnapshots>true</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
如果 maven-dependency-plugin 已经有其他执行(例如,Netbeans 为 javaee-endorsed-api 插入一个),请不要删除它们。
第 2 步)使用 JUnitCore (JUnit4) 以编程方式运行您的测试。
String runTests() {
PrintStream sysOut = System.out;
PrintStream sysErr = System.err;
ByteArrayOutputStream stream = new ByteArrayOutputStream();
PrintStream out = new PrintStream(stream);
try {
System.setOut(out);
System.setErr(out);
TextListener listener = new TextListener(out);
JUnitCore junit = new JUnitCore();
junit.addListener(listener);
junit.run(MyClassIT.class,
AnotherClassIT.class,
...etc...);
} finally {
System.setOut(sysOut);
System.setErr(sysErr);
out.close();
}
return stream.toString();
}
第 3 步)通过 JAX-RS 公开您的测试
@Path("/test")
public class TestResource {
@GET
@Produces("text/plain")
public String getTestResults() {
return runTests();
}
private String runTests() {
...
}
}
将这个类与你的其他测试类(在 src/test 中)放在一起,以便它可以引用它们。
但是,如果您将注册所有资源的 javax.ws.rs.core.Application 类作为子类,则引用 TestResource 时会出现问题(因为源代码无法引用测试代码)。要解决这个问题,请在 src/main/...[same package]... 下创建一个完全空的虚拟 TestResource 类...这个技巧有效,因为虚拟 TestResource 将在打包过程中被真实的覆盖。
public class ShoppingApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
return new HashSet<Class<?>>() {{
add(TestResource.class);
}};
}
@Override
public Set<Object> getSingletons() {
return new HashSet<Object>();
}
}
package ...same package as the real TestResource...
public class TestResource {
}
第 4 步)设置您的 IDE 以启动/部署您的应用,并在构建后自动打开浏览器指向“/test”。