当前位置: 首页 > article >正文

Java-测试-Mockito 入门篇

之前很长一段时间我都认为测试就是使用SpringBootTest类似下面的写法:

@SpringBootTest
class SysAuthServiceTest {
    @Autowired
    SysRoleAuthMapper sysRoleAuthMapper;

    @Test
    public void test() {
        QueryWrapper<SysRoleAuth> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("role_id", 1);
        List<SysRoleAuth> sysRoleAuths = sysRoleAuthMapper.selectList(queryWrapper);
        assertNotNull(sysRoleAuths);
        assert sysRoleAuths.size() > 0;
    }
}

这样也确实是单元测试,只不过这个只是其中的一方面。如果你的某个方法并不依赖于Spring容器,也需要启动整个Spirng环境吗?启动这个环境可能是比较耗时间的。本文对Mockito框架做一个初步探索,找找写单测的感觉。

单元测试规范

https://alibaba.github.io/p3c/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95.html

依赖

本文基于 Junit5 + Mockito 3, 高版本的Mockito 需要高版本的Java支持。比如你使用Mockito 5就需要满足Java版本在11以上。

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.7.7</version>
    <scope>test</scope>
</dependency>

HelloWord - 不需要Spring

在很多时候我们的单元测试是不需Spring的,下面我们来看一个例子。

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class SysRoleServiceTest {
    @Mock
    RedisHelper redisHelper;
    @BeforeEach
    void setUp() {
        when(redisHelper.get(anyString())).thenAnswer((Answer<SysRole>) invocation -> {
            String key = invocation.getArgument(0);
            if ("1836664638416211969".equals(key)) {
                return createRole("admin", "管理员");
            } else {
                return createRole("user", "普通用户");
            }
        });

        when(redisHelper.get(anyString(), anyLong())).thenAnswer((Answer<SysRole>) invocation -> {
            // 通过invocation.getArgument() 获取参数
            String key = invocation.getArgument(0);
            Long id = invocation.getArgument(1);
            if ("1836664638416211969".equals(key) && id == 10L) {
                return createRole("admin", "管理员");
            } else {
                return createRole("user", "普通用户");
            }
        });
    }

    private SysRole createRole(String roleName, String description) {
        SysRole role = new SysRole();
        role.setRoleName(roleName);
        role.setDescription(description);
        role.setCreateTime(new Date());
        role.setUpdateTime(new Date());
        role.setStatus(1);
        role.setLogicDelete(0);
        return role;
    }

    @Test
    void test() {
        String key = "1836664638416211969";
        Object o1 = redisHelper.get(key, 100);
        Object o2 = redisHelper.get(key);
        System.out.println(o1);
        System.out.println(o2);
        assertNotEquals(o1, o2);
    }

}

在这个例子中我们定义了一个RedisHelper,模拟了两个重载方法。为什么要加@MockitoSettings(strictness = Strictness.LENIENT)呢?
如果不加会出现下面的错误,这是因为比如我们模拟了两个方法,但是其中一个方法我们并没有使用:
在这里插入图片描述
这个错误有两个解决方法:1) 你加上前文中的注解 2)你模拟的方法都被使用了,就不会有这个错误

HelloWord - 需要Spring

在一些时候比如我们就是基于Spring环境来写单测,但比如Redis组件还在安装中又或者当前的Redis不可用,我们可以暂时模拟。通过 @MockBean的方式。与上面的区别是:1)我们用的注解不一样 @Mock 和 @MockBean 2)我们需要加SpringBootTest注解

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@SpringBootTest
class SysRoleServiceTest {
    @MockBean
    RedisHelper redisHelper;
    @BeforeEach
    void setUp() {
        when(redisHelper.get(anyString())).thenAnswer((Answer<SysRole>) invocation -> {
            String key = invocation.getArgument(0);
            if ("1836664638416211969".equals(key)) {
                return createRole("admin", "管理员");
            } else {
                return createRole("user", "普通用户");
            }
        });

        when(redisHelper.get(anyString(), anyLong())).thenAnswer((Answer<SysRole>) invocation -> {
            // 通过invocation.getArgument() 获取参数
            String key = invocation.getArgument(0);
            Long id = invocation.getArgument(1);
            if ("1836664638416211969".equals(key) && id == 1L) {
                return createRole("admin", "管理员");
            } else {
                return createRole("user", "普通用户");
            }
        });
    }

    private SysRole createRole(String roleName, String description) {
        SysRole role = new SysRole();
        role.setRoleName(roleName);
        role.setDescription(description);
        role.setCreateTime(new Date());
        role.setUpdateTime(new Date());
        role.setStatus(1);
        role.setLogicDelete(0);
        return role;
    }

    @Test
    void test() {
        String key = "1836664638416211969";
        Object o1 = redisHelper.get(key, 100);
        Object o2 = redisHelper.get(key);
        System.out.println(o1);
        System.out.println(o2);
        assertNotEquals(o1, o2);
    }
}

Mockito 的用法

https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html

模拟对象
  • 模拟行为
List mockedList = mock(List.class);
mockedList.add("one");
verify(mockedList).add("one");
mockedList.stream().forEach(System.out::println);
// 可以指定模拟策略
MockDemo mock = mock(MockDemo.class, Mockito.RETURNS_SMART_NULLS);

这里我模拟一个List对象,特别要注意的是这个对象不存储数据所以后面的流式打印是不会有任何输出的。这个List的对象只在模拟调用方法的这个行为。比如verify(mockedList).add(“one”);就是用来确认这个方法是不是被调用了一次,而前面我们确实调用了一次add方法。那这个有什么用呢?这个可以再不影响数据的前提下测试我们是不是正确的调用了对象的方法,有没有多次调用或者调用次数符不符合预期。
VerificationModeFactory给我们提供了一些验证模型,比如至少多少次atLeast,当然我们是可以拓展自己的规则的。
在这里插入图片描述

  • 模拟对象的方法并指定行为
    我们可以模拟一个对象的方法,并且指定这个方法调用以后的行为,比如返回一个值,抛出一个异常。
LinkedList mockedList = mock(LinkedList.class);
// 返回一个值
when(mockedList.get(0)).thenReturn("first");
// 抛出一个异常
when(mockedList.get(1)).thenThrow(new RuntimeException());
参数匹配

当然我们的参数可以使用通用匹配:
在这里插入图片描述
上面的例子中其实有用到,这里再举个例子,当我们试图从LinkedList 中获取SysRole对象的时候,我们对信息进行处理以后返回:

@Test
public void test() {
    Map mockedList = mock(HashMap.class);
    when(mockedList.get(any(SysRole.class))).thenAnswer(invocation -> {
        SysRole role = invocation.getArgument(0);
        if (role.getRoleName().equals("test")) {
            role.setStatus(1);
            role.setLogicDelete(1);
            role.setDescription("handled");
        }
        return role;
    });
    SysRole sysRole = new SysRole();
    sysRole.setRoleName("test");
    Object o = mockedList.get(sysRole);
    System.out.println(o);
}

又例如当添加的参数是字符串1时添加成功:

List mockedList = mock(List.class);
when(mockedList.add(argThat(str -> str.equals("1")))).thenReturn(true);
Object o = mockedList.add("2");
System.out.println(o);

又比如满足第一个参数是int, 第二个参数是字符串,第三个参数是third argument返回yes

MockDemo demo = mock(MockDemo.class);
when(demo.doSomething(anyInt(), anyString(), eq("third argument"))).thenReturn("yes");
String s = demo.doSomething(1, "two", "third argument");
System.out.println(s);
模拟异常
List mockList = mock(List.class);
when(mockList.add(anyInt())).thenThrow(new RuntimeException("clear error"));
doThrow(new RuntimeException()).when(mockList).clear();

when 和 doThrow 的区别是什么?
when 用于定义方法调用时的返回值或行为。
doThrow 专门用于定义方法调用时抛出的异常

保证顺序

直接引用官网示例:

 // A. Single mock whose methods must be invoked in a particular order
 List singleMock = mock(List.class);
 //using a single mock
 singleMock.add("was added first");
 singleMock.add("was added second");
 //create an inOrder verifier for a single mock
 InOrder inOrder = inOrder(singleMock);
 //following will make sure that add is first called with "was added first", then with "was added second"
 inOrder.verify(singleMock).add("was added first");
 inOrder.verify(singleMock).add("was added second");
 // B. Multiple mocks that must be used in a particular order
 List firstMock = mock(List.class);
 List secondMock = mock(List.class);
 //using mocks
 firstMock.add("was called first");
 secondMock.add("was called second");
 //create inOrder object passing any mocks that need to be verified in order
 InOrder inOrder = inOrder(firstMock, secondMock);
 //following will make sure that firstMock was called before secondMock
 inOrder.verify(firstMock).add("was called first");
 inOrder.verify(secondMock).add("was called second");
模拟链
@Mock
List mockList;

@Test
public void test() {

   when(mockList.add("some arg"))
           .thenThrow(new RuntimeException())
           .thenReturn(true);

   // 第一次调用抛出异常
   try {
       mockList.add("some arg");
   } catch (RuntimeException e) {

   }
   // 第二次调用返回true
   System.out.println(mockList.add("some arg"));
   // 连续调用返回true
   System.out.println(mockList.add("some arg"));
}

在不抛出异常的情况下我们可以进一步简化:

when(mockList.add("some arg")).
        thenReturn(true, false, true);
System.out.println(mockList.add("some arg"));
// 第二次调用返回true
System.out.println(mockList.add("some arg"));
// 连续调用返回true
System.out.println(mockList.add("some arg"));

结果为:true,false,true
我们需要注意和下面的区别:

when(mockList.add("some arg"))
        .thenReturn(true);
when(mockList.add("some arg"))
        .thenReturn(false);
System.out.println(mockList.add("some arg"));
System.out.println(mockList.add("some arg"));

这里第二个会覆盖第一个,结果为false。

真实模拟
List list = new LinkedList();
List spy = spy(list);
when(spy.size()).thenReturn(100);
spy.add("one");
spy.add("two");
System.out.println(spy.get(0));
System.out.println(spy.get(1));
System.out.println(spy.size());

结果:one,two,100
还记得前文我们提到mock是不会真实存储数据的,只是模拟行为,那么如果要存储真实数据用spy();
这里还有个易错点,你可能会像下面这样写,当get的时候返回一个数据,但是结果会抛出异常,如果你有需要获取还没有插入值的spy对象需要使用doReturn

List list = new LinkedList();
List spy = spy(list);
// 这里直接会抛出IndexOutOfBoundsException
when(spy.get(0)).thenReturn("foo");
// 如果想要解决上面的异常使用doReturn
doReturn("foo").when(spy).get(0);
System.out.println(spy.get(0));
捕获验证参数
public class Person {
    private String name;
    public Person(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}
public interface MockDemo {
    void doSomething(Person person);
}
@Test
public void testDoSomething() {
    // 创建模拟对象
    MockDemo mock = mock(MockDemo.class, Mockito.RETURNS_SMART_NULLS);
    // 创建 Person 对象
    Person john = new Person("John");
    // 调用 doSomething 方法
    mock.doSomething(john);
    // 使用 ArgumentCaptor 捕获参数
    ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);

    // 验证 doSomething 方法被调用,并捕获传入的 Person 参数
    verify(mock).doSomething(argument.capture());
    // 验证捕获到的 Person 对象的 name 属性是否为 "John"
    assertEquals("John", argument.getValue().getName());
}

上面这个例子是在调用doSomething方法的时候,捕获到这个参数,并且判断这个参数是否符合我们的预期。
官网有句话值得注意:
it is recommended to use ArgumentCaptor with verification but not with stubbing. Using ArgumentCaptor with stubbing may decrease test readability because captor is created outside of assert (aka verify or ‘then’) block. Also it may reduce defect localization because if stubbed method was not called then no argument is captured.

调用真实方法
when(mock.someMethod()).thenCallRealMethod();
重置
reset(mock);

之前的行为和打桩都会被重置。

注解
// 是下面代码的简写
// List list = new LinkedList();
// List spy = spy(list);
@Spy BeerDrinker drinker;

// 类似于@AutoWired 会进行注入
@InjectMocks LocalPub;

例子:

public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User getUserById(int id) {
        return userRepository.findById(id);
    }
}
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    @Test
    public void testGetUserById() {
        // 创建模拟数据
        User user = new User(1, "John Doe");

        // 设置模拟行为
        when(userRepository.findById(1)).thenReturn(user);

        // 调用方法
        User result = userService.getUserById(1);

        // 验证结果
        assertEquals("John Doe", result.getName());
    }
}

http://www.kler.cn/news/311364.html

相关文章:

  • FTP、SFTP安装,整合Springboot教程
  • 基于剪切板的高速翻译工具
  • 【Qt | QAction】Qt 的 QAction 类介绍
  • 电脑键盘功能基础知识汇总
  • Leetcode面试经典150题-130.被围绕的区域
  • MySql-单表以及多表查询详解
  • paddle 分类网络
  • 【Linux】【Vim】Vim 基础
  • Doris相关记录
  • 【计算机基础题目】二叉树的前序中序后续遍历之间相互转换 详细例子
  • 我的demo保卫萝卜中的技术要点
  • O1-preview:智能预测与预取驱动的性能优化处理器设计OPEN AI
  • Semaphore UI --Ansible webui
  • 心觉:成功学就像一把刀,有什么作用关键在于使用者(二)
  • 进入C++
  • Spring WebFlux实践与源码解析
  • leetcode41. 缺失的第一个正数,原地哈希表
  • Vue2篇
  • 无线感知会议系列【2】【智能无感感知 特征,算法,数据集】
  • 【AI大模型】LLM主流开源大模型介绍
  • 【neo4j】neo4j和Cypher 查询语言相关知识点
  • 【Python】 报错Can‘t find model ‘en_core_web_md‘
  • jmeter吞吐量控制器
  • 大数据新视界 --大数据大厂之SaaS模式下的大数据应用:创新与变革
  • 前端框架对比和选择
  • MiniCPM3-4B | 笔记本电脑运行端侧大模型OpenBMB/MiniCPM3-4B-GPTQ-Int4量化版 | PyCharm环境
  • Redis---卸载Redis
  • Basler 相机与LabVIEW进行集成
  • linux 自动清除日志 脚本
  • 828华为云征文 | 深度评测,华为云Flexus X实例在Sysbench性能测试中的亮眼表现