Tests With Coverage
学习如何度量您应用程序的测试覆盖率。 本指南涵盖:
-
度量您的单元测试的范围
-
度量您集成测试的范围
-
分开执行你的单元测试和集成测试
-
综合所有测试的覆盖率
请注意,native 模式不支持代码覆盖率。
1. 必备条件
要完成本指南,您需要:
-
15 分钟以内
-
IDE
-
用 JAVA_HOME 安装了JDK 1.8+
-
Apache Maven 3.6.2+
-
完成了 测试你的应用程序指南
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
。
GreetingResourceTest
和 GreetingResource
的内容应包括在内。 但当我们打开报告 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.exec 和 jacoco-it.exec)合并成一份综合报告,并生成一份综合报告来显示您所有测试的覆盖率。
你最后应该有这样的东西(注意添加 merge-results
和 post-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>