springboot的单元测试,只是@Test吗?

49 阅读10分钟

SpringBoot中单元测试怎么做?

大纲

对话解析

话题:什么是单元测试?

大佬:什么是单元测试?

凯歌:单元测试是软件开发中的一种测试方法。目的是验证代码中的最小可测试单元(一般是方法或者类)。

话题:黑盒测试和白盒测试是什么?和单元测试有关系吗?

大佬:那什么是黑盒测试,什么是白盒测试?这都是单元测试吗?

凯歌: 黑白盒和单元测试是不同维度的操作,单元测试说的是类型,可以理解为要测试哪些东西。黑白盒测试说的测试策略,可以理解为用什么方法测。

凯歌:黑盒测试和白盒测试是软件测试中,两种不同的测试方法。

凯歌:他们有不同的关注点和适用场景。

凯歌:这两种测试方法都可以用于单元测试,但是不局限于单元测试。也可以用于集成测试、系统测试等其他测试级别。

话题:黑盒测试

大佬:那什么是黑盒测试那?

凯歌:黑盒测试,也可以称为功能测试或数据驱动测试。

凯歌:它不关心程序内部的逻辑结构和代码实现。而是根据需求说明书,来验证程序的功能是否符合要求。

凯歌:测试人员,输入数据,运行程序,检查输出结果和预期是否相符。

凯歌:这种方法主要用于检验软件的功能正确性,以及用户界面、数据库事务、性能等方面的问题。

大佬:哦哦哦,那我明白了,黑盒测试就是看你能不能用,不看你里面是怎么做的。

凯歌:对的,如下简图,只关心结果,不关心过程。

话题:白盒测试

大佬:那什么是白盒测试那?

凯歌:通过名字,我们便可以猜出一二。黑盒测试是不关心过程,只关心结果,那么白盒与之相对,重点便是关注过程。

凯歌: 白盒测试,也称为结构测试或基于代码的测试。

凯歌: 它要求测试人员,了解程序的内部结构和工作原理。包括代码行、路径、条件、循环等。

凯歌: 白盒测试的重点在于确保代码的所有逻辑路径都被执行过,并且在各种可能的情况下都能正常工作。

凯歌: 白盒测试,可以用来查找代码中的错误、安全漏洞、未处理的异常情况等。

大佬: 哦哦哦,白盒测试,就是不仅关注你的结果,而且要看你的过程对不对,比如我提交了一个苹果,然后我要得到一个苹果汁。黑盒测试就是你给我苹果汁就行,别的我不管。白盒测试是我不仅要苹果汁,而且我还要看你是不是用榨汁机做出来。结果、过程它都要。

凯歌:对的。

话题:单元测试特点。

大佬:那单元测试有什么特点呀?

凯歌: 单元测试特点如下

  1. 独立性:单元测试应该可以独立运行,不需要依赖外部系统和资源(数据库系统、文件系统、网络服务等)。通过使用mock对象或stub来模拟这些依赖,确保测试的隔离性。
  2. 自动化:单元测试通常由自动化工具执行的,例如:Junit、TestNG等框架。这些工具,可以自动运行测试用例。并生成详细的测试报告。
  3. 快速执行:单元测试应该设计的足够简洁和高效,,以便可以快速执行。由于他们不依赖外部资源,所以执行速度通常很快,有助于频繁的运行测试以保证代码质量。
  4. 断言机制:单元测试提供了丰富的断言机制,用于验证代码的输出是否符合预期结果。比如:assertEquals、assertTrue、assertFalse等。
  5. 支持Mocking:使用Mockito这样的库,可以轻松创建和管理mock对象,模拟复杂或外部依赖的行为,从而使得单元测试更加纯粹和可靠。
  6. 代码覆盖率分析:通过工具(如JaCoCo)可以分析单元测试对代码的覆盖程度、帮助识别,未被测试的代码部分。
  7. 等等

大佬: 这些都要记住吗?

凯歌: 不用的,有个了解就行,日常用的多了,自己就知道了。主要是Junit、断言、mock和jaCoCo这四个要会使用。

话题:Junit是什么?

大佬:哦哦哦,那你说下Junit吧。

凯歌:好。Junit是Java中一个广泛使用的单元测试框架。

凯歌:下面我们基于Junit5进行讲解。

凯歌:Junit5的组成,主要由三个模块。

Junit5的组成

  1. Junit Platform:这是一个基础层,提供了发现测试、启动测试和执行测试的核心功能。不仅支持Junit框架,还支持别的框架。
  2. Junit Jupiter:API和扩展模型,包含了注解和编程模型。它是Junit5编写测试的主要部分。
  3. Junit Vintage:这个模块,允许在Junit5环境中运行基于Junit4和Junit3编写的测试。

凯歌: 下面,讲解下Junit里面的基本注解

@Test:标记一个方法为测试方法,该方法不接受参数,返回void,并且不能抛出声明的异常。

@BeforeEarch:标识的方法会在每个@Test方法执行前运行,通常用来设置测试环境

@AfterEach:标识的方法会在每个@Test方法执行后运行,通常用来清理资源。

@BeforAll:标识的方法会在所有@Test方法执行前运行一次,通常用于初始化资源,注意这个方法必须是静态

@AfterAll:标记的方法会在所有@Test的方法执行后运行一次,通常用于释放共享资源,同样,这个方法也必须是静态的。

@Disabled:禁用测试,这个方法不运行

@Tag:用于测试分类

凯歌:注解说完,我们再说下断言

常见的断言:

  • assertEquals(expected,actual):检查两个值是否相等
  • assertTrue(condition)?assertFalse(contion):检测条件真?假
  • assertNull(object)?assertNotNull(object):检测对象是否为null
  • assertThrows(exceptionClass,executable):检测代码块是否抛出指定异常
  • assertA():允许一个方法执行多个断言,即使一个失败,其余的也会执行

凯歌:最后我们说一下Junit的参数化测试

Junit5支持参数化测试,通过@ParameterzedTest和@ValueSource、@CsvSource、@MethodSource等注解实现。

@ParamterizedTest
@ValueSource(strings = {"apple","banana","orange"})
void testWithSingleString(String fruit){
  assertNotNUll(fruit)  
}

话题:mock是什么?

大佬:okok,那Junit5我知道个大概了,那你上面说的mock是什么呀?

凯歌:Mock啊,模拟对象的意思,就是造,假对象,你调用的时候,看似执行了方法,实际没有执行。

大佬: 那我们为啥要用假的啊,明明有真的

凯歌:因为我们的代码如果依赖很多第三方系统的时候,如果调用第三方系统有限制,或者说成本高,那么我们不能因为这个就停止测试呀,于是我们需要造个假的,模拟结果,然后往下执行。

大佬:哦哦哦。那具体咋用啊。

凯歌:这个我们在java中一般使用现成的框架,目前比较流行的是Mockito框架。

凯歌:下面我进行一个演示

凯歌:首先在maven中引入依赖

<?xml version="1.0" encoding="UTF-8"?>
<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>JunitDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>4.5.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

</project>

凯歌:第二步是创建需要的user类、service类、mapper类

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class User {
    private int id;
    private String name;
}
public class UserMapper {
    public User findById(int id){
        return new User(2,"鱼皮");
    }
}
public class UserService {
    private final UserMapper userMapper;
    public UserService(UserMapper userMapper){
        this.userMapper = userMapper;
    }
    public User findById(int id){
        return userMapper.findById(id);
    }
}
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

public class UserServiceTest {
    @Mock
    private UserMapper userMapper;
    private UserService userService;

    @BeforeEach
    void setUp(){
        // 初始化mock对象
        MockitoAnnotations.openMocks(this);
        userService = new UserService(userMapper);
    }
    @Test
    void testFindById(){
        // 模拟userMapper的行为
        User mockUser = new User(1, "凯歌");
        // 当userMapper中的findById方法被调用,且参数为1的时候,返回我们创建的mockUser对象
        Mockito.when(userMapper.findById(1)).thenReturn(mockUser);

        // 调用被测试的方法
        User user = userService.findById(1);
        // 验证结果
        Assertions.assertNotNull(user);
        Assertions.assertEquals("凯歌",user.getName());
        // 验证方法是不是被调用了一次
        Mockito.verify(userMapper).findById(1);
    }

    @Test
    void testFindById_UserNotFound(){
        // 设置当id=2 响应null
        Mockito.when(userMapper.findById(2)).thenReturn(null);
        User result = userService.findById(2);
        // 判断id=2的结果是不是null
        Assertions.assertNull(result);
        // 判断是不是执行了id=2的方法
        Mockito.verify(userMapper).findById(2);
    }

    @Test
    void testFindById_ExceptionThrow() {
        // 设置当id=3  抛出异常
        Mockito.when(userMapper.findById(3)).thenThrow(new RuntimeException("Database error"));
        //  判断有没有抛出预期的异常
        Assertions.assertThrowsExactly(RuntimeException.class,()->userService.findById(3));
        //  验证id=3的方法有没有没调用
        Mockito.verify(userMapper).findById(3);
    }
}

凯歌:验证结果如下

话题:JaCoCo怎么用?

凯歌:上面的单元测试我们都说完了,下面我们说说,我们单元测试覆盖率中的一个测试工具,JaCoCo,

大佬:为什么要说这个啊

凯歌:说这个是因为,当我们进行单元测试的时候,如果我们有很多方法,我们在执行完单元测试后,需要知道哪些类测试到了,那么没有,我们如果一个一个看的话,那么多类,看的眼睛都要花了,于是我们需要借助一个工具,帮助我们确定哪些类、哪些方法执行了单元测试,哪些没有。

大佬:哦哦哦,那我明白了,你说吧

凯歌:老样子,我们先建立测试文件,还在刚才的工程上

凯歌:第一步,修改mapper类

public class UserMapper {
    public User findById(int id){
        return new User(2,"鱼皮");
    }
    // 新增jaCoCo方法
    public String jaCoCO(){
        return "JaCoCo测试";
    }
}

凯歌:第二步,修改service方法

public class UserService {
    private final UserMapper userMapper;
    public UserService(UserMapper userMapper){
        this.userMapper = userMapper;
    }
    public User findById(int id){
        return userMapper.findById(id);
    }
  // 新增jaCoCo方法
    public String jaCoCO(){
        return userMapper.jaCoCO();
    }
}

凯歌:第三步,修改测试方法

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

public class UserServiceTest {
    @Test
    void jaCoCoTest() {
        UserService userService = new UserService(new UserMapper());
        String result = userService.jaCoCO();
        Assertions.assertEquals("JaCoCo测试",result);
    }
}

凯歌:整体测试项目目录如下

凯歌:准备好类之后,我们开始引入jaCoCo,jaCoCo是一个插件,于是我们在pom中引入下图即可

<plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.9</version> <!-- 使用最新版本 -->
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <!-- 生成覆盖率报告 -->
                    <execution>
                        <id>report</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

凯歌;这个插件的作用,主要是当我们的单元测试执行完之后,收集我们的单元测试的结果,生成报告

凯歌:在有了这个插件后,我们还需要一个帮我们自动执行单元测试的插件,maven-surefire-plugin,如下

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M7</version>
                <dependencies>
                    <dependency>
                        <groupId>org.apache.maven.surefire</groupId>
                        <artifactId>surefire-api</artifactId>
                        <version>3.0.0-M7</version>
                    </dependency>
                </dependencies>
            </plugin>

凯歌:完整的pom如下

<?xml version="1.0" encoding="UTF-8"?>
<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>JunitDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>4.5.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.9</version> <!-- 使用最新版本 -->
                <executions>
                    <execution>
                        <goals>
                            <goal>prepare-agent</goal>
                        </goals>
                    </execution>
                    <!-- 生成覆盖率报告 -->
                    <execution>
                        <id>report</id>
                        <phase>prepare-package</phase>
                        <goals>
                            <goal>report</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
          <!--  maven默认使用本插件执行单元测试-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M7</version>
                <dependencies>
                    <dependency>
                        <groupId>org.apache.maven.surefire</groupId>
                        <artifactId>surefire-api</artifactId>
                        <version>3.0.0-M7</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>

凯歌:安装完插件后,刷新maven后可以看到下图结构

凯歌:接下来,我们打开maven框,输入命令

mvn clean test jacoco:report

凯歌:执行完上述命令后,查看我们项目的target目录下,会发现多出了几个目录,其中site目录便是我们的覆盖率报告结果所在文件

凯歌:我们进入文件夹,会看到一个index.html目录,双击,我们可以看到右侧界面

凯歌:从右上角的浏览器中随便选择一个打开,便可以看到我们的扫描结果

凯歌:扫描结果如下

凯歌:default是告诉我们整体扫描了多少

凯歌:点开defualt之后,我们可以看到每个类我们的单元测试覆盖率为多少

凯歌:点进类之后,我们可以看到每个方法的单元测试覆盖率,绿色代表单元测试已覆盖,红色代表没有

凯歌:然后点进方法,我们可以看到,方法里具体是哪里没有被单元测试覆盖到