【问题标题】:How to start multiple boot apps for end-to-end tests?如何启动多个启动应用程序进行端到端测试?
【发布时间】:2019-06-18 13:25:05
【问题描述】:

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

已经有效的方法:

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

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

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

我试过了:

  • 在 e2e 模块中为测试的应用添加 maven 依赖项
  • 在每个应用启动的新线程中使用SpringApplication

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

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

我也在考虑启动两个单独的 java -jar ... 进程,但是,如何确保在 2e2 单元测试运行之前构建测试的应用程序 fat-jar?

当前应用程序启动/关闭代码示例,一旦我对第二个应用程序有 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();
            }
        }
    }

【问题讨论】:

    标签: spring-boot end-to-end


    【解决方案1】:

    我认为您的情况,通过 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/

    【讨论】:

    • 我尽量避免使用 docker,对于我的手提电脑来说太重了。
    【解决方案2】:

    我得到了第二个解决方案:

    • 端到端测试项目除了使用TestRestClient 运行 spring-tests 所需的之外没有其他 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;
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2019-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-10-26
      • 2011-04-26
      • 1970-01-01
      • 2015-03-02
      • 2014-11-02
      相关资源
      最近更新 更多