Tests With Coverage

学习如何度量您应用程序的测试覆盖率。 本指南涵盖:

  • 度量您的单元测试的范围

  • 度量您集成测试的范围

  • 分开执行你的单元测试和集成测试

  • 综合所有测试的覆盖率

请注意,native 模式不支持代码覆盖率。

1. 必备条件

要完成本指南,您需要:

2. 结构

本指南生成的应用程序只是一个 JAX-RS 接口(hello world),用依赖注入 service 。 该服务将使用 JUnit 5 进行测试,接口将使用 @QuarkusTest 进行注解。

3. 成果

我们建议您在下面的章节中遵循指示,一步一步创建应用程序。 但是你也可以直接跳到已完成的例子。 克隆 Git 仓库: git clone https://github.com/quarkusio/quarkus-quickstarts.git, 或下载 压缩包

代码在 tests-with-coverage-quickstart 目录

4. 从一个简单的项目和两个测试开始

让我们用 Quarkus Maven 插件创建空应用程序开始:

mvn io.quarkus:quarkus-maven-plugin:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=tests-with-coverage-quickstart
cd tests-with-coverage-quickstart

现在我们将添加所有必要的元素以便应用程序被测试适当覆盖。

首先,应用程序的 hello 接口:

package org.acme.testcoverage;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello")
public class GreetingResource {

    private final GreetingService service;

    @Inject
    public GreetingResource(GreetingService service) {
        this.service = service;
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/greeting/{name}")
    public String greeting(@PathParam("name") String name) {
        return service.greeting(name);
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}

此接口使用 greeting service:

package org.acme.testcoverage;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class GreetingService {

    public String greeting(String name) {
        return "hello " + name;
    }

}

该项目还需要进行一些测试。 第一个简单的 JUnit:

package org.acme.testcoverage;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class GreetingServiceTest {

    @Test
    public void testGreetingService() {
        GreetingService service = new GreetingService();
        Assertions.assertEquals("hello Quarkus", service.greeting("Quarkus"));
    }
}

但也有一个 @QuarkusTest

package org.acme.testcoverage;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;

import java.util.UUID;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;

@QuarkusTest
@Tag("integration")
public class GreetingResourceTest {

    @Test
    public void testHelloEndpoint() {
        given()
          .when().get("/hello")
          .then()
             .statusCode(200)
             .body(is("hello"));
    }

    @Test
    public void testGreetingEndpoint() {
        String uuid = UUID.randomUUID().toString();
        given()
          .pathParam("name", uuid)
          .when().get("/hello/greeting/{name}")
          .then()
            .statusCode(200)
            .body(is("hello " + uuid));
    }
}

第一个将是我们的单元测试,第二个将是我们的集成测试例子。

5. 分开执行单元测试和集成测试

你可能考虑到 JUnit 和 QuarkusTest 是两种不同类型的测试,它们应该被分开。 你可能会分开执行它们,有些比其它更常使用。 为了做到这一点,我们将使用 JUnit 5 的一个功能来标记一些测试。 让我们将 GreetingResourceTest.java tag 下并指定它是一个集成测试:

import org.junit.jupiter.api.Tag;
...

@QuarkusTest
@Tag("integration")
public class GreetingResourceTest {
    ...
}

我们现在能够区分单元测试和集成测试。 现在,让我们把它们绑定到不同的 Maven 生命周期阶段。 让我们使用 surefire 将单元测试绑定到 test 阶段,集成测试绑定到 integration-test 阶段。

<project>
    ...
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <configuration>
                    <excludedGroups>integration</excludedGroups>
                    <systemProperties>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    </systemProperties>
                </configuration>
                <executions>
                    <execution>
                        <id>integration-tests</id>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <excludedGroups>!integration</excludedGroups>
                            <groups>integration</groups>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    ...
</project>
这样, QuarkusTest 实例将作为 integration-test 构建阶段的一部分执行,而其他 JUnit 测试仍将在 test 阶段中运行。 你可以使用命令 ./mvnw clean verify 来运行所有测试(并且你会注意到两个测试正在不同的阶段运行)。

6. 利用 JaCoCo 度量 JUnit 测试覆盖率

现在是引入 JaCoCo 来度量覆盖率的时候了。 在你的 pom.xml build 中直接添加 JaCoCo 插件。

    <properties>
    ...
        <jacoco.version>0.8.4</jacoco.version>
    </properties>

    <build>
        <plugins>
        ...
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.version}</version>
                <executions>
                    <execution>
                        <id>default-prepare-agent</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>default-report</id>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/jacoco.exec</dataFile>
                            <outputDirectory>${project.reporting.outputDirectory}/jacoco</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
如果你运行 ./mvnw clean test,则执行单元测试时收集到的覆盖率在文件 jacoco.exec 中。

7. 分别度量每一种测试类型的覆盖率

这并不是绝对必要的,但让我们区分下每一类测试的覆盖率。 为了做到这一点,我们只需在两个不同的文件中输出覆盖率信息,一个在 jacoco-ut.exec 中,另一个在 jacoco-it.exec 中。 我们还需要为每一次测试执行生成一份单独的报告。 让我们调整 Jacoco 的配置:

    <properties>
    ...
        <jacoco.version>0.8.4</jacoco.version>
    </properties>

    <build>
        <plugins>
        ...
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.version}</version>
                <executions>
                    <execution>
                        <id>prepare-agent-ut</id>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                        <configuration>
                            <destFile>${project.build.directory}/jacoco-ut.exec</destFile>
                        </configuration>
                    </execution>
                    <execution>
                        <id>prepare-agent-it</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                        <configuration>
                            <destFile>${project.build.directory}/jacoco-it.exec</destFile>
                        </configuration>
                    </execution>
                    <execution>
                        <id>report-ut</id>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/jacoco-ut.exec</dataFile>
                            <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
                        </configuration>
                    </execution>
                    <execution>
                        <id>report-it</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
                            <outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

8. 覆盖率似乎不符合现实情况

你现在可以运行测试: ./mvnw clean verify 。正如早些时候解释的那样,它将先运行单元测试,然后进行集成测试。 最后,它将产生两份单独的报告。 首先一份单元测试覆盖率报告在 target/site/jacoco-ut ,然后是一份集成测试覆盖率报告 在 target/site/jacoco-it

GreetingResourceTestGreetingResource 的内容应包括在内。 但当我们打开报告 target/site/jacoco-it/index.html 时,GreetingResource 类的覆盖率为0%。 但报告中 GreetingService 的覆盖率显示的是实际测试中记录的。 怎么来?

在报告生成过程中,您可能已经注意到一个警告:

[WARNING] Classes in bundle '***' do no match with execution data. For report generation the same class files must be used as at runtime.
[WARNING] Execution data for class org/acme/testcoverage/GreetingResource does not match.

看来 Quarkus 和 JaCoCo 是相互接近的。 所发生的情况是,Quarkus 转换 JAX-RS 资源(以及 Panache 文件)。 你可能已经注意到,GreetingResource 不是以最简单的方式编写的:

...
@Path("/hello")
public class GreetingResource {

    @Inject
    GreetingService service;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    @Path("/greeting/{name}")
    public String greeting(@PathParam("name") String name) {
        return service.greeting(name);
    }

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}

从上看,构造函数是隐含的,我们注入了一个 GreetingService 。 请注意,如果这个代码依赖于一个隐式的构造器,则 JaCoCo 会正确报告覆盖率。 相反,我们引入了基于构造器注入:

...
@Path("/hello")
public class GreetingResource {

    private final GreetingService service;

    @Inject
    public GreetingResource(GreetingService service) {
        this.service = service;
    }
...
}

有些人可能会说,这种做法较好,因为这个字段可能是 final 。 无论如何,在某些情况下,你可能需要一个明确的构造函数。 在这种情况下,JaCoCo 没有正确报告覆盖率 。 这是因为 Quarkus 生成了一个没有任何参数的构造函数,并且为了将其添加到类中做了一些字节码修改。 这就是这里发生的事情,就在实施集成测试之前:

[INFO] --- quarkus-maven-plugin:0.16.0:build (default) @ getting-started-testing ---
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Beginning quarkus augmentation
...

因此,JaCoCo 想要创建此报告的时候没有识别到这个类。 但等等…​ 有一种解决办法。

9. Instrumenting the classes instead

JaCoCo 有两种模式。 第一种是以 agent 和 instruments 类为基础运行。 不幸的是,这与 Quarkus 所做的动态类文件转换不兼容 。 第二种模式叫做 offline instrumentation。 在使用类时(当测试运行时)被 Maven goal jacoco:instrument 进行 pre-instrumented 强化, jacocoagent.jar 必须添加到 classpath. 测试一旦执行完毕,建议使用 Maven goal jacoco:restore-instrumented-classes 恢复原来的类。

让我们先添加对 jacocoagent.jar 的依赖:

<project>
    ...
    <dependencies>
        ...
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.agent</artifactId>
            <classifier>runtime</classifier>
            <scope>test</scope>
            <version>${jacoco.version}</version>
        </dependency>
    </dependencies>
</project>

然后让我们为单元测试配置三个 jacoco 插件 goal:

  • process-classes 阶段中 instrument 类

  • 一个将在 prepare-package 阶段(测试运行之后) 恢复原类

  • verify 阶段生成报表(生成报表需要恢复原类)

类似的集成测试设置:

<project>
    ...
    <build>
        <plugins>
            ...
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.version}</version>
                <executions>
                    <execution>
                        <id>instrument-ut</id>
                        <goals>
                            <goal>instrument</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>restore-ut</id>
                        <goals>
                            <goal>restore-instrumented-classes</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report-ut</id>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/jacoco-ut.exec</dataFile>
                            <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
                        </configuration>
                    </execution>
                    <execution>
                        <id>instrument-it</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>instrument</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>restore-it</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>restore-instrumented-classes</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report-it</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
                            <outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

它还需要对 Surefire 的配置略作改变。 请注意,在默认情况下(单元测试)和集成测试中,我们将 jacoco-agent.destfile 作为系统属性。

<project>
    ...
    <build>
        <plugins>
            ...
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <configuration>
                    <excludedGroups>integration</excludedGroups>
                    <systemProperties>
                        <jacoco-agent.destfile>${project.build.directory}/jacoco-ut.exec</jacoco-agent.destfile>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    </systemProperties>
                </configuration>
                <executions>
                    <execution>
                        <id>integration-tests</id>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <excludedGroups>!integration</excludedGroups>
                            <groups>integration</groups>
                            <systemProperties>
                                <jacoco-agent.destfile>${project.build.directory}/jacoco-it.exec</jacoco-agent.destfile>
                            </systemProperties>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

让我们现在来看看生成的报告,可以在 target/site/jacoco-it/index.html 中找到。 报告现在显示,GreetingResource 实际上已经覆盖得很好! 是的!

10. 额外:为单位测试和集成测试编写合并报告

因此,最后让我们进一步改进设置,将两个执行文件 (jacoco-ut.execjacoco-it.exec)合并成一份综合报告,并生成一份综合报告来显示您所有测试的覆盖率。

你最后应该有这样的东西(注意添加 merge-resultspost-merge-report executions):

<project>
    ...
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>${surefire-plugin.version}</version>
                <configuration>
                    <excludedGroups>integration</excludedGroups>
                    <systemProperties>
                        <jacoco-agent.destfile>${project.build.directory}/jacoco-ut.exec</jacoco-agent.destfile>
                        <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
                    </systemProperties>
                </configuration>
                <executions>
                    <execution>
                        <id>integration-tests</id>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <excludedGroups>!integration</excludedGroups>
                            <groups>integration</groups>
                            <systemProperties>
                                <jacoco-agent.destfile>${project.build.directory}/jacoco-it.exec</jacoco-agent.destfile>
                            </systemProperties>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            ...
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>${jacoco.version}</version>
                <executions>
                    <execution>
                        <id>instrument-ut</id>
                        <goals>
                            <goal>instrument</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>restore-ut</id>
                        <goals>
                            <goal>restore-instrumented-classes</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report-ut</id>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/jacoco-ut.exec</dataFile>
                            <outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
                        </configuration>
                    </execution>
                    <execution>
                        <id>instrument-it</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>instrument</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>restore-it</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>restore-instrumented-classes</goal>
                        </goals>
                    </execution>
                    <execution>
                        <id>report-it</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
                            <outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
                        </configuration>
                    </execution>
                    <execution>
                        <id>merge-results</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>merge</goal>
                        </goals>
                        <configuration>
                            <fileSets>
                                <fileSet>
                                    <directory>${project.build.directory}</directory>
                                    <includes>
                                        <include>*.exec</include>
                                    </includes>
                                </fileSet>
                            </fileSets>
                            <destFile>${project.build.directory}/jacoco.exec</destFile>
                        </configuration>
                    </execution>
                    <execution>
                        <id>post-merge-report</id>
                        <phase>verify</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                        <configuration>
                            <dataFile>${project.build.directory}/jacoco.exec</dataFile>
                            <outputDirectory>${project.reporting.outputDirectory}/jacoco</outputDirectory>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    ...
    <dependencies>
        ...
        <dependency>
            <groupId>org.jacoco</groupId>
            <artifactId>org.jacoco.agent</artifactId>
            <classifier>runtime</classifier>
            <scope>test</scope>
            <version>${jacoco.version}</version>
        </dependency>
    </dependencies>
</project>

11. 结论

您现在已经掌握了学习测试覆盖率所需的所有信息! 但请记住,一些没有测试覆盖的代码没有经过很好的检验。 但测试覆盖过的某些代码不一定经过 良好 测试。 请确保写好测试!

quarkus.pro 是基于 quarkus.io 的非官方中文翻译站 ,最后更新 2020/04 。
沪ICP备19006215号-8
QQ交流群:1055930959
微信群: