走进单元测试

单元测试的目的

避免大量人工参与,将测试流程通过脚本、策略、框架来实现自动化测试

为什么要写单元测试

测试一定是在你最担心的地方测试,而不是为了追求测试而去做测试

单元测试是工具而非目标

核心价值:缩短反馈周期、降低缺陷修复成本

如上左图,BUG出现最多的是本地开发阶段,之后的单元测试、上线后bug的

单侧对组织的价值

单侧对个人的价值

单元测试框架

Mock介绍

Mock的作用如下

模拟复杂业务的接口 : 实际工作中如果我们在测试一个接口功能时,如果这个接口依赖个非常复杂的接口业务或者来源于第三方接口 ,那么我们完全可以使用Mock来模拟这个复杂的业务接口,其实这个和解决接口依赖是一样的原理。

前后端联调: 进行前后端分离编程时,如果进行一个前端页面开发,需要根据后台返回的状态展示不同的页面,那么就需要调用后台的接口,但是后台接口还未开发完成,完全可以借助mock来模拟后台这个接口返回想要的数据。

Mock 与Stub(桩)

桩代码(Stub) :用来代替真实代码的临时代码,主要作用是使被测代码能够独立编译链接,并独立运行;

Mock 代码:也是用来代替真实代码的临时代码,起到隔离和补齐的作用,但是它还可深入的模拟对象之间的交互方式,可以对结果进行验证。

认识Mockito

  • Mockito是Java单元测试中使用率最高的Mock框架之一

  • 它通过简明的语法和完整的文档吸引了大量的开发者。Mockito支持用Maven和Gradle来进行依赖引入和管理。

Mockito Maven 引入

复制代码
   <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>3.4.6</version>
            <type>pom</type>
        </dependency>

验证指定行为

Mockito 可以验证某些行为:一旦mock对象被创建了mock对象会记住所有的交互。

复制代码
public class PersonTest {

//     定义一个测试用例叫verifyTest
    @Test
    void verifyTest(){
        Person mockPerson=mock(Person.class);
        mockPerson.setId(1);
        verify(mockPerson).setId(1);
        verify(mockPerson).setName("tom");

    }

}

运行测试用例

错误信息显示你期望person.setName("tom")被调用,但实际只有person.setId(1)被调用了。

‌**错误原因:**‌

  • 你使用verify(person).setName("tom")来验证setName方法是否被调用

  • 但实际测试代码中只调用了person.setId(1),没有调用setName方法

  • Mockito严格检查方法调用,发现预期与实际不符

在测试用例中补充mockPerson.setName("tom");

复制代码
public class PersonTest {

//     定义一个测试用例叫verifyTest
    @Test
    void verifyTest(){
        Person mockPerson=mock(Person.class);
        mockPerson.setId(1);
        mockPerson.setName("tom");
        verify(mockPerson).setId(1);
        verify(mockPerson).setName("tom");

    }

}

指定返回值

Mockito 可以做一些测试桩(Stub):默认情况下,所有的函数都有返回值,mock函数默认返回的是null,一个空的集合或者一个被对象类型包装的内置类型,例如0、false对应的对象类型为Integer、Boolean;

复制代码
public class PersonTest {
//     定义第二个测试用例
    @Test
    void stubTest(){
        Person mockPerson=mock(Person.class);
        when(mockPerson.getId()).thenReturn(1);
        // 定义一个异常返回
        when(mockPerson.getName()).thenThrow(new RuntimeException());
        // 结果会打印出1
        System.out.println(mockPerson.getId());
    }
}

由于我们定义了mockPerson一个行为(调用getName()方法会抛出异常),但是并没有去调用getName()方法,因此运行时不会报错

当我们模拟调用getName()方法时就会报错

复制代码
public class PersonTest {
//     定义第二个测试用例
    @Test
    void stubTest(){
        Person mockPerson=mock(Person.class);
        when(mockPerson.getId()).thenReturn(1);
        // 定义一个异常返回
        when(mockPerson.getName()).thenThrow(new RuntimeException());
        // 结果会打印出1
        System.out.println(mockPerson.getId());
        System.out.println(mockPerson.getName());
    }
}

匹配器

Mockito 以自然的java风格来验证参数值:为了合理的使用复杂的参数匹配,使用equals0)与anyX()的匹配器会使得测试代码更简洁、简单。

复制代码
public class PersonTest {
    @Test
    void matchTest(){
        Person mockPerson=mock(Person.class);
        // 通过anyString()表示匹配任何一个String类型的值,最后我都会返回hello
        when(mockPerson.setKey(anyString())).thenReturn("hello");
        System.out.println(mockPerson.setKey("10"));
        // 验证setKey方法是否被调用
        verify(mockPerson).setKey(anyString());
    }
}

无论传递什么值最后都会输出hello

且 verify(mockPerson).setKey(anyString());验证并未出错,表示方法被正常调用

验证调用次数

Mockito 可以验证函数的确切、最少、从未调用次数:verify函数默认验证的是执行了times(1),也就是某个测试函数是否执行了1次.因此,times(1)通常被省略了。

复制代码
    @Test
    void timesTest(){
        Person mockPerson=mock(Person.class);
        mockPerson.setId(1);
        mockPerson.setName("tom");
        mockPerson.setName("tom");
        // 验证setId方法是否被调用1次
        verify(mockPerson).setId(1);
         // 验证.setName方法是否被调用2次
        verify(mockPerson,times(2)).setName("tom");


    }

验证执行顺序

Mockito 可以验证验证执行执行顺序:验证执行顺序是非常灵活的,不需要一个一个的验证所有交互,只需要验证感兴趣的对象即可。另外,可以仅通过那些需要验证顺序的mock对象来创建InOrder对象。

复制代码
    @Test
    void orderTest(){
        Person singleMockPerson=mock(Person.class);
        singleMockPerson.setName("jack");
        singleMockPerson.setName("tom");
        InOrder inOrder = inOrder(singleMockPerson);
        // 验证先执行的 singleMockPerson.setName("jack")
        // 后执行的 singleMockPerson.setName("tom");
        inOrder.verify(singleMockPerson).setName("jack");
        inOrder.verify(singleMockPerson).setName("tom");

        Person firstMockPerson=mock(Person.class);
        Person secondMockPerson=mock(Person.class);
        firstMockPerson.setId(1);
        secondMockPerson.setId(2);
        InOrder inOrder1 = inOrder(firstMockPerson,secondMockPerson);
        // 验证先执行的secondMockPerson.setId(2)
        // 后执行的 firstMockPerson.setId(1);
        inOrder1.verify(secondMockPerson).setId(2);
        inOrder1.verify(firstMockPerson).setId(1);

    }

模拟连续调用

Mockito 可以为连续的调用做测试桩(stub):有时我们需要为同一个函数调用的不同的返回值或异常做测试桩。

复制代码
    @Test
    void consStubTest(){
        Person mockPerson=mock(Person.class);
        when(mockPerson.getName())
                // 模拟第一次调用时返回jack
                .thenReturn("jack")
                // 模拟第二次调用时返回tom
                .thenReturn("tom");
        // 第一次调用,返回jack
        System.out.println(mockPerson.getName());
        // 第二次调用,返回tom
        System.out.println(mockPerson.getName());
    }

SpringBoot中Mockito应用

  • 从 Spring Boot 项目结构上来说,Service 层是依赖 Dao 层的,而Controller 层又依赖于 Service 层

  • 从单元测试角度,对某个 Service 和 Controler 进行单元的时候,他所有依赖的类都应该进行Mock;而 Dao 层单元测试就比较简单了,只依赖数据库中的数据。

@SpringBootTest 是 Spring Boot 框架中用于集成测试的核心注解,它能够加载完整的 Spring 应用上下文,模拟真实的生产环境来验证业务逻辑和组件交互

使用@Mock注解来Mock对象时的第一种实现,即使用MockitoAnnotations.initMocks(testClass)

复制代码
package com.example.demo;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.context.SpringBootTest;
import static org.mockito.Mockito.*;

// springboot的单元测试用例集合
@SpringBootTest
public class TokenServiceTest {
    @Mock
    private TokenService tokenService;

    // 在每个测试用例执行前
    @BeforeEach
    public void before(){
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void test01(){
        Token token = new Token(1, "test");
        when(tokenService.getTokenById(1)).thenReturn(token);
        System.out.println(tokenService.getTokenById(1).getToken());
        verify(tokenService).getTokenById(1);
    }
}
相关推荐
就叫飞六吧40 分钟前
Spring MVC 接口命名为什么会有 *.do/actions等身影?
java·spring·mvc
万象.41 分钟前
高并发服务器组件单元测试&集成测试&系统测试
服务器·单元测试·集成测试
葡萄成熟时 !1 小时前
黑马学生管理系统
java·开发语言
沐浴露z1 小时前
为什么使用SpringAI时通常用Builder来创建对象?详解 【Builder模式】和【直接 new】的区别
java·python·建造者模式
阿杰真不会敲代码1 小时前
Filter与Interceptor深度解析:分清这两个“拦截器”,面试不再掉坑
java·spring boot·面试
带刺的坐椅2 小时前
Solon AI 开发学习6 - chat - 两种 http 流式输入输出
java·ai·solon
客梦2 小时前
Java 道路信息系统
java·笔记
k***1953 小时前
Tomcat的升级
java·tomcat