Java-测试-Mockito 入门篇

ops/2024/9/19 22:09:20/ 标签: java, 测试, Mockito

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

java">@SpringBootTest
class SysAuthServiceTest {@AutowiredSysRoleAuthMapper sysRoleAuthMapper;@Testpublic 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的,下面我们来看一个例子。

java">@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class SysRoleServiceTest {@MockRedisHelper redisHelper;@BeforeEachvoid 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;}@Testvoid 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注解

java">@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@SpringBootTest
class SysRoleServiceTest {@MockBeanRedisHelper redisHelper;@BeforeEachvoid 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;}@Testvoid 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__143">Mockito 的用法

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

模拟对象
  • 模拟行为
java">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,当然我们是可以拓展自己的规则的。
在这里插入图片描述

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

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

java">@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时添加成功:

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

java">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);
模拟异常
java">List mockList = mock(List.class);
when(mockList.add(anyInt())).thenThrow(new RuntimeException("clear error"));
doThrow(new RuntimeException()).when(mockList).clear();

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

保证顺序

直接引用官网示例:

java"> // A. Single mock whose methods must be invoked in a particular orderList singleMock = mock(List.class);//using a single mocksingleMock.add("was added first");singleMock.add("was added second");//create an inOrder verifier for a single mockInOrder 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 orderList firstMock = mock(List.class);List secondMock = mock(List.class);//using mocksfirstMock.add("was called first");secondMock.add("was called second");//create inOrder object passing any mocks that need to be verified in orderInOrder inOrder = inOrder(firstMock, secondMock);//following will make sure that firstMock was called before secondMockinOrder.verify(firstMock).add("was called first");inOrder.verify(secondMock).add("was called second");
模拟链
java">@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) {}// 第二次调用返回trueSystem.out.println(mockList.add("some arg"));// 连续调用返回trueSystem.out.println(mockList.add("some arg"));
}

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

java">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
我们需要注意和下面的区别:

java">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。

真实模拟
java">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

java">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));
捕获验证参数
java">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.

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

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

注解
java">// 是下面代码的简写
// List list = new LinkedList();
// List spy = spy(list);
@Spy BeerDrinker drinker;// 类似于@AutoWired 会进行注入
@InjectMocks LocalPub;

例子:

java">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 {@Mockprivate UserRepository userRepository;@InjectMocksprivate UserService userService;@BeforeEachpublic void setUp() {MockitoAnnotations.openMocks(this);}@Testpublic 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.ppmy.cn/ops/113139.html

相关文章

工作笔记:Vue 3 中使用 vue-router 进行导航与监听路由变化

一、使用 useRouter 进行导航 在 Vue 3 组件中&#xff0c;你可以使用 useRouter 来方便地进行页面导航。以下是一个简单的示例&#xff1a; <template><button click"navigateToHome">跳转到首页</button> </template><script setup&g…

【C语言】malloc()函数详解(动态内存开辟函数)

&#x1f984;个人主页:修修修也 &#x1f38f;所属专栏:C语言 ⚙️操作环境:Visual Studio 2022 目录 一.malloc()函数简介 1.函数功能 2.函数参数 &#x1f4cc;size_t size 3.函数返回值 4.函数头文件 5.函数生成空间(与calloc区别) 二.malloc()函数的具体使用 1.使…

如何划分 PostgreSQL 数据库权限

PostgreSQL 具有灵活的权限管理机制&#xff0c;可以通过用户、角色以及不同的权限粒度进行划分。合理地划分权限&#xff0c;是 DBA&#xff08;数据库管理员&#xff09;必须掌握的技能&#xff0c;这不仅能确保数据的安全性和完整性&#xff0c;还能提高业务操作的效率。在刚…

JS高级(三)、深浅拷贝,异常处理,this指向总结,改变this指向;节流和防抖

文章目录 一、深浅拷贝1. 浅拷贝&#xff1a;object.assign;解构赋值2. 深拷贝&#xff1a;递归函数、lodash的cloneDeep、JSON 二、异常处理1. throw2. try catch finally 三. this总结1、this的指向2、箭头函数this的指向3、改变函数this的指向 四. 节流和防抖1. 防抖(deboun…

GO 闭包

文章目录 1. **累加器&#xff08;状态保持器&#xff09;**2. **缓存&#xff08;记忆化&#xff09;**3. **工厂函数**4. **函数式编程风格**5. **创建动态行为的函数**6. **控制访问权限** 总结 高级闭包的使用在 Go 中非常灵活和强大&#xff0c;特别是在需要保存状态或对外…

Python 课程11-Web 开发

前言 Web 开发已经成为现代软件开发的核心领域之一&#xff0c;许多应用程序和服务都通过 Web 来与用户和其他系统交互。Python 作为一门广泛使用的编程语言&#xff0c;提供了多种 Web 开发框架&#xff0c;其中最流行的两个框架是 Flask 和 Django。 Flask 是一个轻量级的 W…

Linux命令:对文本文件的内容进行排序的工具sort详解

目录 一、概述 二、用法 1、 基本语法 2、 常用选项 3、获取帮助 三、示例 1. 基本用法 2. 按数字排序 3. 按第二列排序 4. 逆序排序 5. 删除重复行 6. 忽略大小写排序 7. 按人类可读的数值排序 8. 按版本号排序 四、高级用法 1. 与 uniq 结合使用去重 2. 与 gr…

简单题66-加一(Python)20240918

问题描述&#xff1a; python class Solution(object):def plusOne(self, digits):""":type digits: List[int]:rtype: List[int]"""n len(digits)# 从最后一位开始处理进位for i in range(n - 1, -1, -1):if digits[i] < 9:digits[i] 1re…

音视频入门基础:AAC专题(9)——FFmpeg源码中计算AAC裸流每个packet的duration和duration_time的实现

音视频入门基础&#xff1a;AAC专题系列文章&#xff1a; 音视频入门基础&#xff1a;AAC专题&#xff08;1&#xff09;——AAC官方文档下载 音视频入门基础&#xff1a;AAC专题&#xff08;2&#xff09;——使用FFmpeg命令生成AAC裸流文件 音视频入门基础&#xff1a;AAC…

黑神话悟空mac可以玩吗

黑神话悟空mac上能不能玩对于苹果玩家来说很重要&#xff0c;那么黑神话悟空mac可以玩吗&#xff1f;目前是玩不了了&#xff0c;没有针对ios系统的版本&#xff0c;只能之后在云平台上找找了&#xff0c;大家可以再观望下看看。 黑神话悟空mac可以玩吗 ‌使用CrossOver‌&…

c#:System.Text.Json 的使用四(如何忽略[JsonPropertyName])

环境&#xff1a; .net 6.0vs2022 系列篇&#xff1a; 《c#&#xff1a;System.Text.Json 的使用一》 《c#&#xff1a;System.Text.Json 的使用二》 《c#&#xff1a;System.Text.Json 的使用三&#xff08;从Newtonsoft迁移&#xff09;》 《c#&#xff1a;System.Text.Json…

Android Framework(六)WMS-窗口显示流程——窗口内容绘制与显示

文章目录 窗口显示流程明确目标 窗户内容绘制与显示流程窗口Surface的5种状态完整流程图 应用端处理finishDrawingWindow 的触发 system_service处理WindowState状态 -- COMMIT_DRAW_PENDING本次layout 流程简述applySurfaceChangesTransaction 方法概览READY_TO_SHOW和HAS_DRA…

【ARM】SOC的多核启动流程详解

基础概念 • cold boot 冷启动&#xff0c;一上电就开始运行 • warm boot 热启动&#xff0c;只是复位一下 • Primary boot 只给主核跑的那段代码 • Secondary boot 给从核跑的代码 还两种配置&#xff1a; • reset地址是可编程的&#xff0c;则会配置PROGRAMMABLE_RESET_…

Git项目管理工具

分布式版本控制系统 实际操作: 设置用户信息 git config --global user.name “itcast” git config --global user.email "hello@itcast.cn" </

TCP Analysis Flags 之 TCP ZeroWindow

前言 默认情况下&#xff0c;Wireshark 的 TCP 解析器会跟踪每个 TCP 会话的状态&#xff0c;并在检测到问题或潜在问题时提供额外的信息。在第一次打开捕获文件时&#xff0c;会对每个 TCP 数据包进行一次分析&#xff0c;数据包按照它们在数据包列表中出现的顺序进行处理。可…

MacBook苹果电脑安装JDK8、JDK17教程,环境变量配置 + 快速切换JDK版本

MacBook苹果电脑安装JDK8、JDK17教程&#xff0c;环境变量配置 快速切换JDK版本 MacBook M1 安装JDK及环境变量配置 1、在Oracle官网下载JDK 2、安装JDk 3、配置环境变量 4、快速切换 1.下载JDK&#xff08;官网&#xff09; 1.1官网下载dmg安装包 Java Archive | Oracle J…

GPT代码记录

#include <iostream>// 基类模板 template<typename T> class Base { public:void func() {std::cout << "Base function" << std::endl;} };// 特化的子类 template<typename T> class Derived : public Base<T> { public:void…

大模型分离架构学习记录

1、大模型相关名词 TOE&#xff08;TCP Offload Engine&#xff09;是指TCP卸载引擎。它是一种网络技术&#xff0c;通过将TCP/IP协议栈的一部分处理任务从主机的CPU卸载到网卡&#xff1b; 也就是RDMANVLink :在单台服务器内 8 块 GPU 卡通过 NVLink 连接。不同服务器之间的 …

MySQL——数据库的高级操作(一)数据备份与还原(1)数据的备份

在操作数据库时&#xff0c;难免会发生一些意外造成数据丢失。例如&#xff0c;突然停电、管理员的操作失误都可能导致数据的丢失。为了确保数据的安全&#xff0c;需要定期对数据库进行备份&#xff0c;这样&#xff0c;当遇到数据库中数据丢失或者出错的情况&#xff0c;就可…

vue3+ant design vue 中弹窗自定义按钮设置及以冒号为基准布局

1、自定义弹窗按钮&#xff0c;去除取消和确定按钮。&#xff08;网上很多方法都是说通过插槽来实现&#xff0c;但是试了下不生效&#xff0c;那既然插槽不生效的话&#xff0c;干脆直接写按钮就好了&#xff09; <a-modalv-model:open"open"title"人员信息…