No one can guarantee their code is foolproof unless they have 100% execution coverage — not Lu Xun

In project development, we expect to be able to unit test interfaces and code, and require code coverage of more than 80%. The idea is that if someone else changes the code later, and it affects other systems, we can expose the problem when the code is unit tested. So we expect developers running unit tests locally to see how much coverage their code has, and to show exactly where and what is not covered. This article describes using the Jacoco plug-in to generate unit test coverage reports and view coverage details.

1. Create a Maven project


      
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>JacocoDemo</artifactId>
  <version>1.0 the SNAPSHOT</version>

  <properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <jacoco.version>0.8.6</jacoco.version>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>Against 2.4.1</version>
  </parent>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <! -- junit 5 -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-api</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-params</artifactId>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter-engine</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <! -- jacoco: generate test coverage report -->
      <plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>${jacoco.version}</version>
        <executions>
          <execution>
            <goals>
              <goal>prepare-agent</goal>
            </goals>
          </execution>
          <execution>
            <id>generate-code-coverage-report</id>
            <phase>test</phase>
            <goals>
              <goal>report</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
Copy the code

We used Junit5 and created a web project to test it, which I noted in the pom.

2. Create a SpringBoot boot class

org.example.jacocodemo.App:

package org.example.jacocodemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) { SpringApplication.run(App.class, args); }}Copy the code

3. Create a Controller:

DemoController:

package org.example.jacocodemo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/test")
public class DemoController {

    @GetMapping(path = "/{branchId}")
    public String getBranch(@PathVariable("branchId") int branchId) {
        if (branchId == 0) {
            return "master";
        } else if (branchId == 1) {
            return "dev";
        }
        return "release"; }}Copy the code

The idea here is very simple. We divide code branches through the branchId passed to us, and achieve different code coverage by designing different test cases.

4. Create unit tests

DemoControllerTest:

package org.example.jacocodemo;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class DemoControllerTest {
    static MockMvc mockMvc;

    @BeforeAll
    static void setUp(a) {
        mockMvc = MockMvcBuilders.standaloneSetup(new DemoController()).build();
    }

    @Test
    void testGetBranch(a) {
        Map<Integer, String> testCases = new HashMap<>();
        testCases.put(0."master");
        testCases.put(1."dev");
        testCases.put(3."release");
        testCases.forEach((key, value) -> {
            try {
                mockMvc
                    .perform(MockMvcRequestBuilders.get("/test/{branchId}". key) .accept(MediaType.APPLICATION_JSON) .characterEncoding(StandardCharsets.UTF_8.name())) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.content().string(value)) .andReturn(); }catch(Exception e) { e.printStackTrace(); }}); }}Copy the code

The syntax of junit5 is used, the SpringBootTest and ExtendWith annotations are used to launch unit tests, the use of random ports is specified, and the Controller calls are simulated via MockMvc. Initialization of MockMvc is done in setUp, using a Map to simulate test cases and expected results.

5. Run unit tests

Execute in the project root directorymvn clean testTo execute the unit tests and generate the coverage report, which is./target/site/jacoco/index.htmlOpen the file in a browser to see the overall coverage and unit test status:Click on our DemoController hierarchy to see code coverage and branch coverage. Click into the file and select method to see which lines of code are executed in the method:

Since SpringBoot uses junit4 by default, the Mvn-surfire-maven-plugin does not execute my unit test when I run the MVN Clean test in junit4. After checking some solutions, they did not take effect, and finally changed to junit5. You can also handle and modify according to the technology used in your own projects. If you have a solution to the problem of unit tests not executing under junit4, please leave a comment.

reference

  1. How to make Maven unit test code coverage work
  2. Spring Boot learning notes (5) – write unit tests with JUnit5