1. 概述

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

2. 服务层

2.1. 服务类

我们将创建服务并添加一些方法:

@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";
    }

}

这些方法的命名表明:

  • 在同一层的单元测试(/java-unit-testing-best-practices)将测试unitTestedOnly()方法。
  • 单元测试将测试coveredByUnitAndIntegrationTests()。控制器模块中的一个集成测试也会覆盖这个方法的代码。
  • 集成测试将覆盖coveredByIntegrationTest()。然而,没有单元测试会测试这个方法。
  • notTested()方法没有任何测试覆盖。

2.2. 单元测试

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

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());
    }

}

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

2.3. Surefire插件配置

我们将使用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. 控制器层

接下来,我们在多模块应用中添加一个控制器层。

3.1. 控制器类

让我们添加控制器类:

@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();
    }

}

fullyTested()indirectlyTestingServiceMethod()方法将由集成测试进行测试。因此,**这些测试将覆盖服务方法coveredByUnitAndIntegrationTests()coveredByIntegrationTest()**。另一方面,我们将不为notTested()编写任何测试。

3.2. 集成测试

现在我们可以测试我们的RestController

@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"));
    }

}

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

3.3. Failsafe插件配置

我们将使用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. 通过Jacoco聚合覆盖率

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

4.1. 准备Jacoco代理

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

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

4.2. 收集测试结果

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

由于准备阶段,我们可以聚合每个模块的报告。这将是report-aggregate目标的工作:

<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>

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

$ mvn clean verify

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

打开index.html文件查看结果。

首先,我们可以导航到控制器类的报告:

控制器覆盖率

正如预期的那样,构造函数和fullyTested()indirectlyTestingServiceMethod()方法被测试覆盖,而notTested()没有被覆盖。

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

服务覆盖率

这次,让我们关注coveredByIntegrationTest()方法。我们知道,服务模块中没有测试这个方法的测试。唯一通过这个方法代码的测试是在控制器模块中。然而,Jacoco识别到了对这个方法的测试。在这种情况下,聚合的意义得到了充分展现!

5. 总结

在这篇文章中,我们创建了一个多模块项目,并借助Jacoco收集了测试覆盖率。

请记住,我们需要在运行测试之前运行准备阶段,而在之后进行聚合。要深入了解,我们可以使用工具如SonarQube来获得覆盖率结果的概览。

一如既往,代码可在GitHub上获取。