在JUnit 5上用TestContainers测试一个Java项目的方法

224 阅读3分钟

我们的测试类做了以下假设。被测试的代码代表了一个具有管理To Do列表的端点的Web服务。测试用例与这些端点交互,通过对它们进行HTTP调用来验证正确的行为。

TestContainers库提供了丰富的API来启动容器作为测试夹具。该API包括建立Dockerfile的功能,从Dockerfile创建一个镜像,并为该镜像启动一个容器。列表3中的测试代码使用JUnit 5兼容的注解来实现这一点。

ToDoWebServiceFunctionalTest.java

@Testcontainers
public class ToDoWebServiceFunctionalTest {
    private final static File DISTRIBUTION_DIR = new File(System.getProperty("distribution.dir"));
    private final static String ARCHIVE_NAME = System.getProperty("archive.name");

    @Container
    private GenericContainer appContainer = createContainer();

    private static GenericContainer createContainer() {
        return new GenericContainer(buildImageDockerfile())
                .withExposedPorts(8080)
                .waitingFor(Wait.forHttp("/actuator/health")
                .forStatusCode(200));
    }

    private static ImageFromDockerfile buildImageDockerfile() {
        return new ImageFromDockerfile()
                .withFileFromFile(ARCHIVE_NAME, new File(DISTRIBUTION_DIR, ARCHIVE_NAME))
                .withDockerfileFromBuilder(builder -> builder
                        .from("openjdk:jre-alpine")
                        .copy(ARCHIVE_NAME, "/app/" + ARCHIVE_NAME)
                        .entryPoint("java", "-jar", "/app/" + ARCHIVE_NAME)
                        .build());
    }
}

清单3.创建并启动一个容器作为测试夹具

我想指出清单中几个有趣的部分。Docker文件只包括三个指令。它使用名为openjdk:jre-alpine 的基础镜像,以使重置的镜像尽可能的小。然后,它复制了JAR文件并将java 命令指向它作为入口。在启动容器时,测试执行会被阻止,直到Spring Boot应用程序变得 "健康"。在这种情况下,应用程序使用Actuator来暴露一个健康状态端点。

使用JAR文件在容器中运行应用程序可能不是性能最好的选择。对源代码的每一次修改都会导致需要重建存档。

更好的选择是以一种为外部依赖、资源文件和类文件创建独立层的方式定义Dockerfile。这样一来,Docker就可以缓存未改变的层了。你也会想禁止自动删除TestContainers的图像

测试案例的责任是通过进行HTTP调用和检查响应来验证应用程序的端点。GenericContainer 类提供了检索容器的 IP 地址和暴露的端口的方法。有了这些信息,你就可以确定容器所暴露的端点URL,如清单4中所示。

ToDoWebServiceFunctionalTest.java

@Test
@DisplayName("can retrieve all items before and after inserting new ones")
void retrieveAllItems() {
    // Use endpoint URL to make HTTP calls
}

private URL buildEndpointUrl(String context) {
    StringBuilder url = new StringBuilder();
    url.append("http://");
    url.append(appContainer.getContainerIpAddress());
    url.append(":");
    url.append(appContainer.getFirstMappedPort());
    url.append(context);

    try {
        return new URL(url.toString());
    } catch (MalformedURLException e) {
        throw new RuntimeException("Invalid URL", e);
    }
}

清单 4.构建容器的端点URL

从构建中执行测试,效果很好。但在IDE中运行测试呢?

从IDE执行测试 开发人员在IDE中生活和呼吸。不幸的是,IDE和构建工具之间的集成并不像它所能做到的那样紧密。例如,如果您从IDE执行相同的测试用例,则会遇到类似的异常,如下所示。

java.lang.ExceptionInInitializerError
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
Caused by: java.lang.NullPointerException
	at java.base/java.io.File.<init>(File.java:276)
	at com.bmuschko.todo.webservice.ToDoWebServiceFunctionalTest.<clinit>(ToDoWebServiceFunctionalTest.java:23)
	... 44 more

显然,我们在构建文件中设置的系统属性无法解析。即使您创建了系统属性,在更改源代码时仍然会遇到问题。默认情况下,从IDE执行测试不会创建JAR文件。因此,最新的更改不会反映在二进制文件中,从而导致错误的行为。 那么,如何从IDE运行测试?实际上,目前唯一的选择是将测试执行委托给构建。在IntelliJ中,此设置是可配置的。