如何启动多个引导应用程序进行端到端测试?

时间:2019-06-18 13:25:05

标签: spring-boot end-to-end

我想编写端到端测试,以验证两个启动应用程序与各种配置文件一起正常工作。

已经起作用的东西:

  • 除了两个经过测试的应用程序(授权服务器和资源服务器)之外,创建用于端到端测试的第三个maven模块(e2e)
  • 使用TestResTemplate编写测试

如果我手动启动授权服务器和资源服务器,则测试工作正常。

我现在想做的是使用正确的配置文件为每个测试自动化经过测试的启动应用程序的启动和关闭。

我尝试过:

  • 将maven依赖项添加到e2e模块中经过测试的应用程序中
  • 在新线程中使用SpringApplication来启动每个应用程序

但是我遇到配置错误的问题,因为所有资源和依赖项都以相同的共享类路径结尾...

有没有办法解决这个问题?

我还在考虑启动两个单独的java -jar ...进程,但是,然后如何确保在运行2e2单元测试之前构建经过测试的应用胖子呢?

当前应用程序启动/关闭代码示例在我对第二个应用程序具有maven依赖关系后立即失败:

    private Service startAuthorizationServer(boolean isJwtActive) throws InterruptedException {
        return new Service(
                AuthorizationServer.class,
                isJwtActive ? new String[]{ "jwt" } : new String[]{} );
    }

    private static final class Service {
        private ConfigurableApplicationContext context;
        private final Thread thread;

        public Service(Class<?> appClass, String... profiles) throws InterruptedException {
            thread = new Thread(() -> {
                SpringApplication app = new SpringApplicationBuilder(appClass).profiles(profiles).build();
                context = app.run();

            });
            thread.setDaemon(false);
            thread.start();
            while (context == null || !context.isRunning()) {
                Thread.sleep(1000);
            };
        }

        @PreDestroy
        public void stop() {
            if (context != null) {
                SpringApplication.exit(context);
            }
            if (thread != null) {
                thread.interrupt();
            }
        }
    }

2 个答案:

答案 0 :(得分:0)

我认为您的情况是通过docker compose运行两个应用程序是一个好主意。 本文介绍了如何使用docker compose映像设置一些集成测试:https://blog.codecentric.de/en/2017/03/writing-integration-tests-docker-compose-junit/

另外,请看一下马丁·福勒(Martin Fowler)的这篇文章:https://martinfowler.com/articles/microservice-testing/

答案 1 :(得分:0)

我有第二种解决方案:

  • 端到端测试项目除了使用TestRestClient进行弹簧测试外,没有其他maven依赖项。
  • 测试配置初始化环境,在单独的进程中的必需模块上运行mvn package
  • 测试用例在单独的java -jar ...进程中使用选定的配置文件运行(重新)启动应用程序

这是我为此编写的帮助程序类(取from there):

class ActuatorApp {
    private final int port;
    private final String actuatorEndpoint;
    private final File jarFile;
    private final TestRestTemplate actuatorClient;
    private Process process;

    private ActuatorApp(File jarFile, int port, TestRestTemplate actuatorClient) {
        this.port = port;
        this.actuatorEndpoint = getBaseUri() + "actuator/";
        this.actuatorClient = actuatorClient;
        this.jarFile = jarFile;

        Assert.isTrue(jarFile.exists(), jarFile.getAbsolutePath() + " does not exist");
    }

    public void start(List<String> profiles, List<String> additionalArgs) throws InterruptedException, IOException {
        if (isUp()) {
            stop();
        }

        this.process = Runtime.getRuntime().exec(appStartCmd(jarFile, profiles, additionalArgs));

        Executors.newSingleThreadExecutor().submit(new ProcessStdOutPrinter(process));

        for (int i = 0; i < 10 && !isUp(); ++i) {
            Thread.sleep(5000);
        }
    }

    public void start(String... profiles) throws InterruptedException, IOException {
        this.start(Arrays.asList(profiles), List.of());
    }

    public void stop() throws InterruptedException {
        if (isUp()) {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
            headers.setAccept(List.of(MediaType.APPLICATION_JSON_UTF8));

            actuatorClient.postForEntity(actuatorEndpoint + "shutdown", new HttpEntity<>(headers), Object.class);
            Thread.sleep(5000);
        }
        if (process != null) {
            process.destroy();
        }
    }

    private String[] appStartCmd(File jarFile, List<String> profiles, List<String> additionalArgs) {
        final List<String> cmd = new ArrayList<>(
                List.of(
                        "java",
                        "-jar",
                        jarFile.getAbsolutePath(),
                        "--server.port=" + port,
                        "--management.endpoint.heath.enabled=true",
                        "--management.endpoint.shutdown.enabled=true",
                        "--management.endpoints.web.exposure.include=*",
                        "--management.endpoints.web.base-path=/actuator"));
        if (profiles.size() > 0) {
            cmd.add("--spring.profiles.active=" + profiles.stream().collect(Collectors.joining(",")));
        }
        if (additionalArgs != null) {
            cmd.addAll(additionalArgs);
        }
        return cmd.toArray(new String[0]);
    }

    private boolean isUp() {
        try {
            final ResponseEntity<HealthResponse> response =
                    actuatorClient.getForEntity(actuatorEndpoint + "health", HealthResponse.class);
            return response.getStatusCode().is2xxSuccessful() && response.getBody().getStatus().equals("UP");
        } catch (ResourceAccessException e) {
            return false;
        }
    }

    public static Builder builder(String moduleName, String moduleVersion) {
        return new Builder(moduleName, moduleVersion);
    }

    /**
     * Configure and build a spring-boot app
     *
     * @author Ch4mp
     *
     */
    public static class Builder {

        private String moduleParentDirectory = "..";

        private final String moduleName;

        private final String moduleVersion;

        private int port = SocketUtils.findAvailableTcpPort(8080);

        private String actuatorClientId = "actuator";

        private String actuatorClientSecret = "secret";

        public Builder(String moduleName, String moduleVersion) {
            this.moduleName = moduleName;
            this.moduleVersion = moduleVersion;
        }

        public Builder moduleParentDirectory(String moduleParentDirectory) {
            this.moduleParentDirectory = moduleParentDirectory;
            return this;
        }

        public Builder port(int port) {
            this.port = port;
            return this;
        }

        public Builder actuatorClientId(String actuatorClientId) {
            this.actuatorClientId = actuatorClientId;
            return this;
        }

        public Builder actuatorClientSecret(String actuatorClientSecret) {
            this.actuatorClientSecret = actuatorClientSecret;
            return this;
        }

        /**
         * Ensures the app module is found and packaged
         * @return app ready to be started
         * @throws IOException if module packaging throws one
         * @throws InterruptedException if module packaging throws one
         */
        public ActuatorApp build() throws IOException, InterruptedException {
            final File moduleDir = new File(moduleParentDirectory, moduleName);

            packageModule(moduleDir);

            final File jarFile = new File(new File(moduleDir, "target"), moduleName + "-" + moduleVersion + ".jar");

            return new ActuatorApp(jarFile, port, new TestRestTemplate(actuatorClientId, actuatorClientSecret));
        }

        private void packageModule(File moduleDir) throws IOException, InterruptedException {
            Assert.isTrue(moduleDir.exists(), "could not find module. " + moduleDir + " does not exist.");

            String[] cmd = new File(moduleDir, "pom.xml").exists() ?
                    new String[] { "mvn", "-DskipTests=true", "package" } :
                    new String[] { "./gradlew", "bootJar" };

            Process mvnProcess = new ProcessBuilder().directory(moduleDir).command(cmd).start();
            Executors.newSingleThreadExecutor().submit(new ProcessStdOutPrinter(mvnProcess));

            Assert.isTrue(mvnProcess.waitFor() == 0, "module packaging exited with error status.");
        }
    }

    private static class ProcessStdOutPrinter implements Runnable {
        private InputStream inputStream;

        public ProcessStdOutPrinter(Process process) {
            this.inputStream = process.getInputStream();
        }

        @Override
        public void run() {
            new BufferedReader(new InputStreamReader(inputStream)).lines().forEach(System.out::println);
        }
    }

    public String getBaseUri() {
        return "https://localhost:" + port;
    }
}
相关问题