在服务器上运行单元测试 (JAX-RS)

2022-09-02 01:17:15

我正在编写一个JAX-RS(Jersey+Maven)应用程序,它可以执行一些棘手的事情(例如调用WAR中嵌入的本机可执行文件)。我需要在服务器(运行Tomcat 7.0.22的Amazon Elastic Beanstalk)上运行[一些]我的单元测试(JUnit4)来检查一切正常。

除了RYO(自己滚动)之外,有没有一种标准的,灵活的方法来做到这一点?我发现的事情似乎更多地与开发人员机器上的集成测试有关(即泽西测试框架)。甚至RYO也让我感到困惑...如何从源包调用测试包中的代码?

基本上,我想创建一个可以调用的 /test 资源,该资源将以漂亮的格式从服务器返回我的单元测试结果。如果我能做/test/{category}就更好了


答案 1

我想分享我在发布这个问题后学到的东西,并在StackExchange上提出了我的第一个答案(在这个网站上,我通过谷歌无数次到达该网站,寻找解决我无休止的问题的方法)

单元、集成、功能测试连续体

在这个问题上有很多纠正,争论和恶搞,所以我想澄清一下。这一切都非常简单。假设你有一些服务。当你称之为它时,有一连串的事件,我将简单地说明如下:

(已接收请求) - (调用函数 1) - (调用函数 2) - (调用函数 3) - (已发送响应)

单元测试单独测试每个函数(或类或单元),输入输入并检查输出。集成测试需要几个单元(例如函数 2-函数 3 链),并且还执行 ol' 进和出。功能测试贯穿整个链,从请求到响应。我将留给读者来猜测在每个规模级别进行测试的一些优点和缺点。无论如何,所有这些测试都可以在服务器中运行,并且有充分的理由在那里运行它们。

容器内/服务器内测试的类型

  • 测试中的容器Spring和其他依赖关系注入框架的一个功能允许您为每个测试设置一个仅填充最小类(加上所有模拟)的容器。这非常方便,因为它消除了手动布线的需要,并且可以更好地接近生产环境。这只允许单元和集成测试。
    • 优点:a)传统的单元测试(具有集中和隔离测试的优点)变得更加方便 b)由于您正在测试自动布线逻辑,因此更接近生产环境 e)与IDE测试运行程序集成 f)快速
    • 缺点:a)环境可能与生产环境有很大不同 b)不能取代对功能测试的需求
  • 测试中的服务器一个普通的测试运行程序运行几乎普通的单元测试,这些单元测试启动嵌入式服务器或容器,并对其进行调用。一些框架(如泽西测试框架)只允许功能测试,但大多数(Arquillian,jeeunit)允许你做所有类型。使用其中一些框架,就好像测试在服务器上运行一样,可以进行任何类型的调用。
    • 优点(除了您可以访问所有容器和服务器服务之外):a)您具有独立的测试,不需要安装或设置任何内容b)测试是隔离的,因为为每个测试或测试套件创建了一个新的服务器/容器。b) 与 IDE 测试运行程序集成
    • 缺点:a)环境可能与生产环境有很大不同(例如,Jetty不是Tomcat或Glassfish)b)启动/停止服务器会减慢测试速度c)框架很糟糕。Jeeunit是一个小项目,甚至没有在Windows上进行测试,Arquillian很大,但非常新,文档很差,我也无法让它工作。
  • 服务器中的测试在这里,测试实际上是与代码一起编译并一起运行的。
    • 优点:a)你有普通的,旧的测试,不需要知道或使用任何类型的框架
    • 缺点:a)测试之间没有隔离(不一定是问题,甚至是缺点,但可能必须采取预防措施) b)不与IDE测试运行程序集成(至少在Netbeans中)
    • 在构建过程中使用 MavenMaven启动一个服务器,加载你的特殊测试WAR,执行测试,并给出一个很好的Surefire报告。
      • 其他优点:a)它在构建过程中完成(并将与持续集成工具和其他工具集成)b)无需安装或设置任何内容(Maven将自动下载,运行等服务器)
      • 其他缺点:a)环境可能相当不同(Maven使用Jetty,它在你的机器上运行)b)无法在生产中重新运行
    • 战中测试测试是与您的代码一起永久编译的。无论何时何地,无论您的WAR何时何地,您都可以启动测试。在开发服务器上,在过渡期间,甚至在生产中。这就是我最初的问题。
      • 其他优点:a)完全正确的环境。b) 随时运行测试
      • 其他缺点:a)需要设置服务器

还有一点要说。Netbeans 将 Maven 测试的大部分优势与 WAR 中测试相结合。它包括一个嵌入式服务器,并在构建后自动启动和部署到它。它甚至打开了火狐...只需将其设置为指向 /test 资源即可。这就像以Maven的方式做一样,但更好。

无论如何,我将向您展示如何在同一个Maven项目中同时进行Maven测试和WAR测试。

使用弹簧的容器测试:

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 文件的路径时,这是必需的,因为 WebAppConfiguration 只允许您在编译时静态设置路径。在服务器中运行测试时使用这个类(见下文),我注入了真正的ServletContext。我使用Spring的配置文件功能通过环境变量(不是很优雅)来抑制它。setServletContext 仅由服务器测试运行程序调用。MockServletContextAwareProcessor

@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) 您可以以不同的方式命名它们,但这是默认情况下的 Failafe 所期望的。IT 代表集成测试,但测试代码可以位于测试连续体中的任何位置。例如,您可以实例化一个类并对其进行单元测试,或者您可以启动HttpClient(或泽西岛客户端),将其指向自己(请注意下面的端口),并在功能上测试您的入口点。

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-failafe插件和maven-jetty插件添加到您的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中:(改编自这里))

<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依赖项插件提供额外的执行来包含测试库,其目标是包含测试范围的复制依赖性

<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/...[同一包装]...这个技巧之所以有效,是因为在打包过程中,虚拟的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”。


答案 2

获胜的关键词原来是“容器内测试”。这个崭新而卓越的框架是Arquillian

奇怪的是,似乎没有别的。StackOverflow上的其他人问道:“我没有看到这些项目使用得太广泛,所以容器内测试有什么不好的地方吗?但没有得到明确的答复。

我想这只是单元测试和完全集成测试这两个大领域之间的一个小区域,需要通过容器内测试来覆盖。对我来说,我也只需要几个测试来检查服务器资源是否可访问和正常运行。可能应该手工编写它们,而不是花所有这些时间研究(然后学习)容器内测试。


推荐