Maven Multi-Module Project Coverage With Jacoco – 使用 Jacoco 对 Maven 多模块项目进行覆盖

最后修改: 2023年 9月 5日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll build a Maven multi-module project. In this project, the services and controllers will be in different modules. Then, we’ll write some tests and use Jacoco to calculate the code coverage.

在本教程中,我们将构建一个 Maven 多模块项目。在该项目中,服务和控制器将位于不同的模块中。然后,我们将编写一些测试,并使用 Jacoco 计算代码覆盖率。

2. Service Layer

2.服务层

First, let’s create the service layer of our multi-module application.

首先,让我们创建多模块应用程序的服务层。

2.1. Service Class

2.1.服务等级

We’ll create our service and add a couple of methods:

我们将创建服务并添加几个方法:

@Service
class MyService {

    String unitTestedOnly() {
        return "unit tested only";
    }

    String coveredByUnitAndIntegrationTests() {
        return "covered by unit and integration tests";
    }

    String coveredByIntegrationTest() {
        return "covered by integration test";
    }

    String notTested() {
        return "not tested";
    }

}

As their names indicate:

正如它们的名字所示:

  • a unit test located in the same layer will test the method unitTestedOnly()
  • a unit test will test coveredByUnitAndIntegrationTests(). An integration test in the controller module will also cover this method’s code
  • an integration test will cover coveredByIntegrationTest(). However, no unit test will test this method
  • No test will cover the method notTested()

2.2. Unit Tests

2.2.单元测试

Let’s now write the corresponding unit tests:

现在让我们编写相应的单元测试:

class MyServiceUnitTest {

    MyService myService = new MyService();
    
    @Test
    void whenUnitTestedOnly_thenCorrectText() {
        assertEquals("unit tested only", myService.unitTestedOnly());
    }

    @Test
    void whenTestedMethod_thenCorrectText() {
        assertEquals("covered by unit and integration tests", myService.coveredByUnitAndIntegrationTests());
    }

}

The tests are simply checking that the method’s output is as expected.

测试只是检查方法的输出是否符合预期。

2.3. Surefire Plugin Configuration

2.3.信火插件配置

We’ll use the Maven Surefire plugin to run the unit tests. Let’s configure it inside the service’s module pom.xml:

我们将使用 Maven Surefire 插件来运行单元测试。让我们在服务的模块 pom.xml 中配置它:

<plugins>
    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.1.2</version>
    <configuration>
    <includes>
        <include>**/*Test.java</include>
    </includes>
    </configuration>
    </plugin>
</plugins>

3. Controller Layer

3.控制器层

We’ll now add a controller layer in our multi-module application.

现在,我们将在多模块应用程序中添加一个控制器层。

3.1. Controller Class

3.1.控制器类

Let’s add the controller class:

让我们添加控制器类:

@RestController
class MyController {

    private final MyService myService;

    public MyController(MyService myService) {
        this.myService = myService;
    }

    @GetMapping("/tested")
    String fullyTested() {
        return myService.coveredByUnitAndIntegrationTests();
    }

    @GetMapping("/indirecttest")
    String indirectlyTestingServiceMethod() {
        return myService.coveredByIntegrationTest();
    }

    @GetMapping("/nottested")
    String notTested() {
        return myService.notTested();
    }

}

The fullyTested() and indirectlyTestingServiceMethod() methods will be tested by integration tests. As a result, those tests will cover the two service methods coveredByUnitAndIntegrationTests() and coveredByIntegrationTest(). On the other hand, we’ll write no test for notTested().

集成测试将测试 fullyTested()indirectlyTestingServiceMethod() 方法。因此,这些测试将涵盖两个服务方法 coveredByUnitAndIntegrationTests()coveredByIntegrationTest() 。另一方面,我们将不为 notTested() 编写测试。

3.2. Integration Tests

3.2.集成测试

We can now test our RestController:

我们现在可以测试我们的 RestController: <br

@SpringBootTest(classes = MyApplication.class)
@AutoConfigureMockMvc
class MyControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void whenFullyTested_ThenCorrectText() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/tested"))
          .andExpect(MockMvcResultMatchers.status()
            .isOk())
          .andExpect(MockMvcResultMatchers.content()
            .string("covered by unit and integration tests"));
    }

    @Test
    void whenIndirectlyTestingServiceMethod_ThenCorrectText() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/indirecttest"))
          .andExpect(MockMvcResultMatchers.status()
            .isOk())
          .andExpect(MockMvcResultMatchers.content()
            .string("covered by integration test"));
    }

}

In these tests, we start an application server and send it requests. Then, we check that the output is correct.

在这些测试中,我们启动应用服务器并向其发送请求。然后检查输出是否正确。

3.3. Failsafe Plugin Configuration

3.3.故障安全插件配置

We’ll use the Maven Failsafe plugin to run the integration tests. The last step is to configure it inside the controller’s module pom.xml:

我们将使用 Maven Failsafe 插件来运行集成测试。最后一步是在控制器模块 pom.xml 中进行配置:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>3.1.2</version>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
	</execution>
        </executions>
    <configuration>
        <includes>
            <include>**/*IntegrationTest.java</include>
        </includes>
    </configuration>
</plugin>

4. Aggregating Coverage via Jacoco

4.通过 Jacoco 聚合承保范围

Jacoco (Java Code Coverage) is a tool used in Java applications to measure code coverage during testing. Let’s now compute our coverage report.

Jacoco(Java 代码覆盖率)是 Java 应用程序中的一种工具,用于在测试过程中测量代码覆盖率。现在让我们计算一下覆盖率报告。

4.1. Preparing Jacoco Agent

4.1.准备 Jacoco 代理

The prepare-agent phase sets up the necessary hooks and configuration so that Jacoco can track the executed code while running tests. This configuration is required before we run any tests. Thus, we’ll add the preparation step directly to the parent pom.xml:

prepare-agent阶段设置必要的钩子和配置,以便 Jacoco 可以在运行测试时跟踪已执行的代码在运行任何测试之前,都需要进行该配置。因此,我们将直接在父 pom.xml 中添加准备步骤:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.8</version>
    <executions>
        <execution>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
    </executions>
</plugin>

4.2. Gathering Tests Results

4.2.收集测试结果

To gather the test coverage, we’ll create a new module aggregate-report. It contains only a pom.xml and has dependencies on the two previous modules.

为了收集测试覆盖率,我们将创建一个新模块 aggregate-report。它只包含一个 pom.xml,并依赖于前两个模块。

Thanks to the preparation phase, we can aggregate the reports from every module. It’s the job of the report-aggregate goal:

有了准备阶段,我们就能汇总每个模块的报告。这是 report-aggregate goal 的工作:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.8</version>
    <executions>
        <execution>
            <phase>verify</phase>
            <goals>
                <goal>report-aggregate</goal>
            </goals>
            <configuration>
                <dataFileIncludes>
                    <dataFileInclude>**/jacoco.exec</dataFileInclude>
                </dataFileIncludes>
                <outputDirectory>${project.reporting.outputDirectory}/jacoco-aggregate</outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

We can now run the verify goal from the parent module:

现在,我们可以从父模块运行 verify 目标:

$ mvn clean verify

At the end of the build, we can see that Jacoco generates the report in the target/site/jacoco-aggregate folder of the aggregate-report submodule.

在构建结束时,我们可以看到 Jacoco 在 target/site/jacoco-aggregate 子模块的 aggregate-report 文件夹中生成了报告。

Let’s open the index.html file to have a look at the results.

让我们打开 index.html 文件,看看结果如何。

To begin, we can navigate to the report for the controller class:

首先,我们可以浏览控制器类的报告:

controller coverage

controller coverage

As expected, the constructor and the fullyTested() and indirectlyTestingServiceMethod() methods are covered by the tests, whereas notTested() isn’t covered.

不出所料,构造函数、fullyTested()indirectlyTestingServiceMethod() 方法已被测试覆盖,而 notTested() 则未被覆盖。

Let’s now have a look at the report for the service class:

现在让我们来看看服务类的报告:

service coverage

服务覆盖范围

This time, let’s focus on the coveredByIntegrationTest() method. As we know, no test in the service module tests this method. The only test that passes through this method’s code is inside the controller module. However, Jacoco recognized that there is a test for this method. In this case, the word aggregation takes its whole meaning!

这次,让我们重点关注 coveredByIntegrationTest() 方法。我们知道,服务模块中的任何测试都不会测试该方法。唯一通过该方法代码的测试位于控制器模块中。但是,Jacoco 发现该方法存在一个测试。在这种情况下,”聚合 “一词就有了完整的含义!

5. Conclusion

5.结论

In this article, we created a multi-module project and gathered the test coverage thanks to Jacoco.

在本文中,我们创建了一个多模块项目,并利用 Jacoco 收集了测试覆盖率。

Let’s recall that we need to run the preparation phase before the tests, while the aggregation takes place after them. To go further, we can use a tool like SonarQube to get a nice overview of the coverage result.

让我们回顾一下,我们需要在测试之前运行准备阶段,而在测试之后进行聚合。为了更进一步,我们可以使用SonarQube这样的工具来获得覆盖结果的概览。

As always, the code is available over on GitHub.

与往常一样,代码可在 GitHub 上获取。