美文网首页
Vert.x 导论之四:单元测试和集成测试

Vert.x 导论之四:单元测试和集成测试

作者: luciensun | 来源:发表于2018-06-10 22:25 被阅读0次

    ’Vert.x导论‘回顾

    现在让我们快速回顾到目前为止在Vert.x导论系列中我们开发了些什么。在第一篇帖子中,我们开发了一个非常简单的Vert.x 3应用,并且学习了这个应用如何被测试,打包及执行。在第二篇帖子中,我们学习了这个应用如何可配置,并在测试中采用了随机端口。最后,在上一篇帖子中,展示了如何使用vertx-web以及如何实现一个小型的REST API。然而,我们忘记了一个重要任务。我们没有测试新增的API。在这篇帖子中,我们会通过实现单元测试以及集成测试来增加我们对新功能的信心。

    这篇帖子的代码在Introduction-to-Vert.x-Demo项目的post-4分支。起始代码在post-3分支。

    测试,测试,再测试。。。

    这篇帖子主要关于测试。我们区分两种测试:单元测试和集成测试。两者同等重要,但是关注点不同。单元测试确保你应用的一个组件正常工作,通常就是Java世界内的一个class行为符合预期。应用并没有作为一个整体被测试,而是一部分一部分的测试。集成测试感觉更黑盒测试因为应用通常从外部启动和测试。

    在这篇帖子中,我们将从更多的单元测试起步作为热身,然后聚焦于集成测试。如果你之前实现过集成测试,你可能会被吓到,这说得通。但不用怕,用Vert.x开发没有隐藏的惊吓。

    热身:更多的单元测试

    我们慢慢来。在第一篇帖子里,我们用vertx-unit实现了一个单元测试。我们之前做的这个测试超级简单:

    1. 我们在测试前启动了应用
    2. 我们检验它是否以"Hello"作为响应

    为了方便你回忆,让我们看看这段代码

    @Before
    public void setUp(TestContext context) throws IOException {
      vertx = Vertx.vertx();
      ServerSocket socket = new ServerSocket(0);
      port = socket.getLocalPort();
      socket.close();
      DeploymentOptions options = new DeploymentOptions()
          .setConfig(new JsonObject().put("http.port", port)
          );
      vertx.deployVerticle(MyFirstVerticle.class.getName(), options, context.asyncAssertSuccess());
    }
    

    setUp方法在每次测试前都会被调用(@Before注解指定这样操作)。这个方法首先创建一个Vert.x的新 实例,然后获取一个可用端口,最后根据对应的配置来部署我们的verticle。context.asyncAssertSuccess()方法会一直等待直到verticle被成功部署好为止。

    tearDown方法是简单明了的,只是关闭了Vert.x实例。它自动卸载了verticles:

    @After
    public void tearDown(TestContext context) {
      vertx.close(context.asyncAssertSuccess());
    }
    

    最终,我们的单个测试是:

    @Test
    public void testMyApplication(TestContext context) {
      final Async async = context.async();
      vertx.createHttpClient().getNow(port, "localhost", "/", response -> {
        response.handler(body -> {
          context.assertTrue(body.toString().contains("Hello"));
          async.complete();
        });
      });
     }
    

    这个测试只是检测当我们对"/"地址发送一个HTTP请求时,应用是否回复了"Hello"。现在我们尝试实现一些单元测试来确认我们的web应用和REST API接口的行为是否符合预期。我们首先检查"index.html"页面是否正确工作。这个测试和之前那个测试很相似。

    @Test
    public void checkThatTheIndexPageIsServed(TestContext context) {
      Async async = context.async();
      vertx.createHttpClient().getNow(port, "localhost", "/assets/index.html", response -> {
        context.assertEquals(response.statusCode(), 200);
        context.assertEquals(response.headers().get("Content-Type"), "text/html");
        response.bodyHandler(body -> {
          context.assertTrue(body.toString().contains("<title>My Whisky Collection</title>"));
          async.complete();
        });
      });
    }
    

    我们检索了index.html页面并检查:

    1. 页面存在(状态码200)
    2. 这是个HTML页面(Content-Type被设置为"text/html")
    3. 页面的标题正确("My Whisky Collection")
      检索内容
      如你所见,我们可以在HTTP响应上直接测试状态码和消息头,但我们需要检索消息体来确保它是正确的。这通过接受整个消息体作为参数的消息体句柄来做到的。一旦最后的检验完成,我们通过调用complete来释放async
      很好,但这实际上并没有测试我们的REST API。先确认我们可以在集合中增加一瓶葡萄酒。不像之前的测试,这个测试使用post方法post数据到服务器:
    @Test
    public void checkThatWeCanAdd(TestContext context) {
      Async async = context.async();
      final String json = Json.encodePrettily(new Whisky("Jameson", "Ireland"));
      final String length = Integer.toString(json.length());
      vertx.createHttpClient().post(port, "localhost", "/api/whiskies")
          .putHeader("content-type", "application/json")
          .putHeader("content-length", length)
          .handler(response -> {
            context.assertEquals(response.statusCode(), 201);
            context.assertTrue(response.headers().get("content-type").contains("application/json"));
            response.bodyHandler(body -> {
              final Whisky whisky = Json.decodeValue(body.toString(), Whisky.class);
              context.assertEquals(whisky.getName(), "Jameson");
              context.assertEquals(whisky.getOrigin(), "Ireland");
              context.assertNotNull(whisky.getId());
              async.complete();
            });
          })
          .write(json)
          .end();
    }
    

    首先我们创建我们想要添加的内容。服务器消费JSON数据,所以我们需要一个JSON字符串。你可以手工写出你的JSON文档,或者和这里一样使用Vert.x方法(Json.encodePrettily)。一旦我们准备好了内容,我们做一个POST请求。我们需要配置一些消息头来确保我们的JSON数据被服务器正确读取。我们表示我们在发送JSON数据并且还设置了消息体的长度。我们还附加了一个响应句柄做了类似前面测试的检测。请注意我们可以使用JSON.decodeValue方法将服务器发送的JSON文档重构成我们需要的对象。这样做可以避免很多样板代码所以很方便。此刻,请求还没有发送,我们需要写出数据并调用end()方法。这通过 .write(json).end();来办到。

    方法的顺序很重要。如果你没有配置好响应句柄,你不能写出数据。最后不要忘记调用end()

    你可以使用如下命令来执行测试:

    mvn clean test
    

    我们可以写更多类似这样的单元测试,但这将变得很复杂。下面将使用集成测试来继续我们的测试工作。

    集成测试很伤人

    我想我们首先需要明确,集成测试很折磨人。如果你在这个领域有经验,你还记得要花多久让一切事物就绪?一想起这事我就头疼。为何集成测试越来越麻烦了?主要在于安装环节:

    1. 我们必须以近似生产环境的方式来启动应用
    2. 接下来要运行测试(配置测试确保检查的是所需的应用实例)
    3. 最后必须停止应用

    听上去并不麻烦,但如果你需要Linux,MacOS X和Windows的支持,事情很快变得凌乱起来。有很多了不起的框架可以解决这个问题比如Arquillian,但这里我们将不使用框架做集成测试,以便更好的理解工作机理。

    我们需要一份战斗计划

    在投入复杂的配置前,我们先花点时间确认下任务:
    第一步 - 保留一个可用端口 我们需要获取一个应用可以监听的可用端口,并且我们需要将这个端口注入到集成测试中。

    第二步 - 生成应用配置 一旦准备好了可用端口,我们需要写一个JSON文件配置这个端口为应用的HTTP端口

    第三步 - 启动应用 听起来很容易?由于我们需要在后台进程中启动应用,所以也并不那么简单。

    第四步 - 执行集成测试 最后,重点部分,运行测试。但在这之前,我们应该事先一些集成测试。我们后面将会提到。

    第五步 - 停止应用 一旦测试都执行完成,无论测试中是否有失败或错误,我们需要停止应用。

    有多种方式可以实现这份计划。我们打算采用一种通用的方式。这也许不是最好的,但几乎可以在任何场合使用。这种方法和Apache Maven绑的很紧。如果你想提议一种替代方案(采用Gradle或者其他工具),我很高兴能把你的方法添加到这篇帖子中。

    实现这份计划

    如上所说,这章节以Maven为中心,大部分代码在pom.xml文件中。如果你从未使用过不同的Maven生命周期阶段,推荐你读一下introduction to the Maven lifecycle

    我们需要添加和配置一些插件。打开pom.xml文件,在<plugins>部分添加:

    <plugin>
      <groupId>org.codehaus.mojo</groupId>
      <artifactId>build-helper-maven-plugin</artifactId>
      <version>1.9.1</version>
      <executions>
        <execution>
          <id>reserve-network-port</id>
          <goals>
            <goal>reserve-network-port</goal>
          </goals>
          <phase>process-sources</phase>
          <configuration>
            <portNames>
              <portName>http.port</portName>
            </portNames>
          </configuration>
        </execution>
      </executions>
    </plugin>
    

    我们使用build-helper-maven-plugin(如果你经常使用Maven你应该去了解下)来获取一个可用端口。一旦确定,这个插件将可用端口赋值给http.port变量。我们在构建过程的早期执行这个插件(在process-sources阶段),这样我们可以在其他插件中使用http.port变量。这是为了第一步做准备。

    第二步需要执行两个动作。首先,在pom.xml文件中,紧跟在<build>开放标签下,添加:

    <testResources>
      <testResource>
        <directory>src/test/resources</directory>
        <filtering>true</filtering>
      </testResource>
    </testResources>
    

    这里指示Maven从 src/test/resources目录过滤资源。Filter意味着用真实值代替占位符。这正是我们所需的,现在我们有了http.port变量。现在用如下内容来创建 src/test/resources/my-it-config.json文件:

    {
      "http.port": ${http.port}
    }
    

    这个配置文件类似于我们在之前帖子中创建的那个。唯一的差别在于${http.port},这也是Maven过滤用的默认语法。所以,当Maven需要处理文件时,它将会用被选的端口来替换${http.port}。这就是第二步。

    第三步和第五步的处理比较麻烦。我们要启动和停止应用。我们打算用maven-antrun-plugin来办到。在pom.xml文件中,在build-helper-maven-plugin下,添加:

    <!-- We use the maven-antrun-plugin to start the application before the integration tests
    and stop them afterward -->
    <plugin>
      <artifactId>maven-antrun-plugin</artifactId>
      <version>1.8</version>
      <executions>
        <execution>
          <id>start-vertx-app</id>
          <phase>pre-integration-test</phase>
          <goals>
            <goal>run</goal>
          </goals>
          <configuration>
            <target>
              <!--
              Launch the application as in 'production' using the fatjar.
              We pass the generated configuration, configuring the http port to the picked one
              -->
              <exec executable="${java.home}/bin/java"
                    dir="${project.build.directory}"
                    spawn="true">
                <arg value="-jar"/>
                <arg value="${project.artifactId}-${project.version}-fat.jar"/>
                <arg value="-conf"/>
                <arg value="${project.build.directory}/test-classes/my-it-config.json"/>
              </exec>
            </target>
          </configuration>
        </execution>
        <execution>
          <id>stop-vertx-app</id>
          <phase>post-integration-test</phase>
          <goals>
            <goal>run</goal>
          </goals>
          <configuration>
            <!--
              Kill the started process.
              Finding the right process is a bit tricky. Windows command is in the windows profile (below)
              -->
            <target>
              <exec executable="bash"
                    dir="${project.build.directory}"
                    spawn="false">
                <arg value="-c"/>
                <arg value="ps ax | grep -Ei '[\-]DtestPort=${http.port}\s+\-jar\s+${project.artifactId}' | awk 'NR==1{print $1}' | xargs kill -SIGTERM"/>
              </exec>
            </target>
          </configuration>
        </execution>
      </executions>
    </plugin>
    

    这里有一大堆XML。我们为这个插件配置了两个执行阶段。第一个,在pre-integration-test阶段,执行一系列bash命令来启动应用。主要是执行:

    java -jar my-first-app-1.0-SNAPSHOT-fat.jar -conf .../my-it-config.json
    

    fatfar被创建了?
    嵌入了我们应用的fatfar在package阶段被创建,在pre-integration-test之前,所以,fatjar是被创建了。
    如上,我们如在生产环境一样启动了应用。

    一旦集成测试被执行了(第四步我们还没说起),我们需要停止应用(所以在post-integration-test阶段)。为了关闭应用,我们会使用一些shell魔法命令来查找我们的进程号,会用到ps命令并发送SIGTERM信号,这些等同于:

    ps
    .... -> find your process id
    kill your_process_id -SIGTERM
    

    还有Windows?
    我之前提起过,我们希望支持Windows而这些命令在Windows下不工作。不用担心,Windows配置在下文会提到...
    我们现在将要做之前跳过的第四步。为了执行我们的集成测试,我们将使用maven-failsafe-plugin。将如下插件配置添加到你的pom.xml文件中:

    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-failsafe-plugin</artifactId>
      <version>2.18.1</version>
      <executions>
        <execution>
          <goals>
            <goal>integration-test</goal>
            <goal>verify</goal>
          </goals>
          <configuration>
            <systemProperties>
              <http.port>${http.port}</http.port>
            </systemProperties>
          </configuration>
        </execution>
      </executions>
    </plugin>
    

    如你所见,我们将http.port属性作为一个系统变量传递,这样我们的测试能够连接到正确的端口。

    就这样了,现在来试试(就Windows用户而言,你必须更有耐心或直接跳到最后一节)。

    mvn clean verify
    

    我们不该使用 mvn integration-test 因为这样应用不会停止。verify阶段在post-integration-test阶段后,会分析集成测试的结果。由于集成测试失败造成的构建失败会在这阶段报告。

    我们还没有具体的集成测试内容!

    我们准备好了集成测试所需的材料,但我们还没有一个集成测试。为了简化实现,我们使用两个库:AssertJRest-Assured

    AssertJ提供很多断言,这些断言你能够链化并顺畅使用。Rest Assured是一个用来测试REST API的框架。

    pom.xml文件中,在</dependencies>前添加如下两个依赖:

    <dependency>
      <groupId>com.jayway.restassured</groupId>
      <artifactId>rest-assured</artifactId>
      <version>3.0.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <version>3.10.0</version>
      <scope>test</scope>
    </dependency>
    

    然后创建 src/test/java/io/vertx/blog/first/MyRestIT.java 文件。不像单元测试,集成测试以IT结束。对Failsafe插件来说,很容易区分单元测试(以Test开始结束)和集成测试(以IT开始结束)。在新增的文件中添加:

    package io.vertx.blog.first;
    
    import com.jayway.restassured.RestAssured;
    import org.junit.AfterClass;
    import org.junit.BeforeClass;
    
    public class MyRestIT {
    
      @BeforeClass
      public static void configureRestAssured() {
        RestAssured.baseURI = "http://localhost";
        RestAssured.port = Integer.getInteger("http.port", 8080);
      }
    
      @AfterClass
      public static void unconfigureRestAssured() {
        RestAssured.reset();
      }
    }
    

    @BeforeClass@AfterClass注解的方法在类里所有的测试之前/之后分别执行一次。这里,我们只是取回http.port(作为系统参数传入)并配置REST Assured。

    是时候实现一个真的测试。让我们检测是否可以获取某个特定产品:

    @Test
    public void checkThatWeCanRetrieveIndividualProduct() {
      // Get the list of bottles, ensure it's a success and extract the first id.
      final int id = RestAssured.get("/api/whiskies").then()
          .assertThat()
          .statusCode(200)
          .extract()
          .jsonPath().getInt("find { it.name=='Bowmore 15 Years Laimrig' }.id");
      // Now get the individual resource and check the content
      RestAssured.get("/api/whiskies/" + id).then()
          .assertThat()
          .statusCode(200)
          .body("name", equalTo("Bowmore 15 Years Laimrig"))
          .body("origin", equalTo("Scotland, Islay"))
          .body("id", equalTo(id));
    }
    

    这里你能够欣赏Rest Assured的力量和表达力。我们获取产品列表,确认响应是正确的,使用JSON(Groovy)路径表达式来提取某个特定产品的id。

    然后,我们尝试获取这个产品的元数据,并检验结果。

    现在实现一个更复杂的场景。添加和删除一个产品:

    @Test
    public void checkWeCanAddAndDeleteAProduct() {
      // Create a new bottle and retrieve the result (as a Whisky instance).
      Whisky whisky = RestAssured.given()
          .body("{\"name\":\"Jameson\", \"origin\":\"Ireland\"}").request().post("/api/whiskies").thenReturn().as(Whisky.class);
            Assertions.assertThat(whisky.getName()).isEqualToIgnoringCase("Jameson");
        Assertions.assertThat(whisky.getOrigin()).isEqualToIgnoringCase("Ireland");
        Assertions.assertThat(whisky.getId()).isNotZero();
      // Check that it has created an individual resource, and check the content.
      RestAssured.get("/api/whiskies/" + whisky.getId()).then()
          .assertThat()
          .statusCode(200)
          .body("name", equalTo("Jameson"))
          .body("origin", equalTo("Ireland"))
          .body("id", equalTo(whisky.getId()));
      // Delete the bottle
      RestAssured.delete("/api/whiskies/" + whisky.getId()).then().assertThat().statusCode(204);
      // Check that the resource is not available anymore
      RestAssured.get("/api/whiskies/" + whisky.getId()).then()
          .assertThat()
          .statusCode(404);
    }
    

    现在我们有了集成测试,试着输入如下命令:

    mvn clean verify
    

    还蛮简单的?等环境被准备好后是蛮简单的。。。你能够继续实现其他集成测试来确保一切行为如你预期。

    亲爱的Windows用户...

    这一节是给Windows用户的福利,还有想在Windows机器上运行他们的集成测试的人们。之前我们执行来停止应用的命令在Windows系统上不起作用。幸运的是,我们可以用一个在Windows系统上执行的profile来扩展pom.xml。

    在你的pom.xml文件中,紧跟着</build>,添加:

    <profiles>
      <!-- A profile for windows as the stop command is different -->
      <profile>
        <id>windows</id>
        <activation>
          <os>
            <family>windows</family>
          </os>
        </activation>
        <build>
          <plugins>
            <plugin>
              <artifactId>maven-antrun-plugin</artifactId>
              <version>1.8</version>
              <executions>
                <execution>
                  <id>stop-vertx-app</id>
                  <phase>post-integration-test</phase>
                  <goals>
                    <goal>run</goal>
                  </goals>
                  <configuration>
                    <target>
                      <exec executable="wmic"
                          dir="${project.build.directory}"
                          spawn="false">
                        <arg value="process"/>
                        <arg value="where"/>
                        <arg value="CommandLine like '%${project.artifactId}%' and not name='wmic.exe'"/>
                        <arg value="delete"/>
                      </exec>
                    </target>
                  </configuration>
                </execution>
              </executions>
            </plugin>
          </plugins>
        </build>
      </profile>
    </profiles>
    

    这个profile用适用于Windows系统的版本替换了之前描述的版本来停止应用。这个profile在Windows上自动启用。和在其他操作系统上一样,执行:

    mvn clean verify
    

    如果pom.xml配置文件有

    Plugin execution not covered by lifecycle configuration: 
    org.codehaus.mojo:build-helper-maven-plugin:1.12:reserve-network-port
     (execution: reserve-network-port, phase: process-sources)
    

    这样的报错信息。
    这是因为m2e对maven的阶段支持不好造成的,具体可以参考m2e-execution-not-covered。具体修正代码如下:

    <pluginManagement>
      <plugins>
        <plugin>
         <groupId>org.eclipse.m2e</groupId>
         <artifactId>lifecycle-mapping</artifactId>
         <version>1.0.0</version>
         <configuration>
           <lifecycleMappingMetadata>
             <pluginExecutions>
               <pluginExecution>
                 <pluginExecutionFilter>
                   <groupId>org.codehaus.mojo</groupId>
                   <artifactId>build-helper-maven-plugin</artifactId>
                   <versionRange>[${build-helper.maven-plugin.version},)</versionRange>
                   <goals>
                     <goal>reserve-network-port</goal>
                   </goals>
                 </pluginExecutionFilter>
                 <action>
                   <ignore/>
                 </action>
               </pluginExecution>
             </pluginExecutions>
           </lifecycleMappingMetadata>
         </configuration>
        </plugin>
      </plugins>
    </pluginManagement>
    

    <article class="col-xs-12 blog-post">

    <article>

    结论

    我们完成了...在这个帖子中,我们看到通过实现单元测试和集成测试,我们对自己的Vert.x应用更有信心了。单元测试,由于vert.x-unit,能够检测Vert.x应用的异步特性,但在复杂场景下可能太复杂。感谢Rest Assured和AssertJ,集成测试写起来简单很多...但是准备过程不够直观。这篇帖子展示了如何配置集成测试环境。很明显,你也能够在单元测试中使用AssertJ和Rest Assured。

    next post中,我们用一个数据库来取代内存后端,并和数据库进行异步集成。

    敬请期待!

    相关文章

      网友评论

          本文标题:Vert.x 导论之四:单元测试和集成测试

          本文链接:https://www.haomeiwen.com/subject/aogmsftx.html