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

Java-测试-Mockito 入门篇

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

@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的,下面我们来看一个例子。

@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注解

@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 的用法

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 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");
模拟链
@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"));
}

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

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 {@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.lryc.cn/news/441551.html

相关文章:

  • 【jupyter notebook】环境部署及pycharm连接虚拟机和本地两种方式
  • TypeScript异常处理
  • go的学习笔记
  • 卷积和转置卷积的输出尺寸计算
  • vue3+ts 使用amCharts展示地图,1.点击左侧国家,可以高亮并放大右侧地图对应的国家。 2.展示数据球。
  • 汽车无钥匙启动功能工作原理
  • C++标准的一些特性记录:C++11的auto和decltype
  • 【Elasticsearch系列四】ELK Stack
  • 【新手上路】衡石分析平台使用手册-认证方式
  • 数字电路与逻辑设计-触发器功能测试及其应用
  • 【网站架构部署与优化】web服务与http协议
  • 【字符函数】strcpy函数(字符串复制函数)+strcat函数(字符串追加)+strcmp函数(字符串比较)【笔记】
  • codetop字符串刷题,刷穿地心!!不再畏惧!!暴打面试官!!
  • 快速体验Linux发行版:DistroSea详解与操作指南
  • Java设计模式—面向对象设计原则(二) --------> 里氏代换原则 LSP (完整详解,附有代码+案列)
  • 使用ShardingSphere实现MySql的分库分表
  • 为什么 Feign 要用 HTTP 而不是 RPC?
  • OJ在线评测系统 前端开发设计优化通用菜单组件二 调试用户自动登录
  • mongodb 安装教程
  • 切换淘宝最新镜像源npm
  • SpringAI-基于java大模型的胡言乱语
  • python提问及解析
  • 从Apple Intelligence到IoT Intelligence,端侧生成式AI时代加速到来
  • 智能AC管理系统HTTPD-AC 1.0服务存在未授权访问漏洞
  • Sidewalk介绍
  • Java数据结构 (泛型第二节) 泛型擦除机制/泛型的限制/上界下界
  • 数据安全标准在非结构化数据中台的遵守
  • 探索Go语言中的Goroutine并发机制
  • 实现实时Web应用,使用AJAX轮询、WebSocket、还是SSE呢??
  • 3GPP协议入门——物理层基础(一)