目录
- 一、简介
- 1.1 单元测试的特点
- 1.2 mock类框架使用场景
- 1.3 常用mock类框架
- 1.3.1 mockito
- 1.3.2 easymock
- 1.3.3 powermock
- 1.3.4 JMockit
- 二、mockito的单独使用
- 2.1 mock对象与spy对象
- 2.2 初始化mock/spy对象的方式
- 初始化mock/spy对象第1种方式
- 初始化mock/spy对象第2种方式
- 初始化mock/spy对象第3种方式
- 2.3 参数匹配
- 2.4 方法插桩
- 返回指定值
- void返回值方法插桩
- 插桩的两种方式
- 抛异常
- 多次插桩
- thenAnswer
- 执行真正的原始方法
- verify的使用
- 2.5 @InjectMocks注解的使用
- 断言工具
- 三、实战讲解
- 四、mockito在springboot环境使用(不推荐)
- 附上其他代码
此文根据视频《mockito加junit搞定单元测试》进行整理,如有侵权,联系删除。
一、简介
1.1 单元测试的特点
- 配合断言使用(杜绝System.out) 。
- 可重复执行 。
- 不依赖环境 。
- 不会对数据产生影响。
- spring的上下文环境不是必须的 。
- 一般都需要配合mock类框架来实现。
1.2 mock类框架使用场景
要进行测试的方法存在外部依赖(如db,redis,第三方接口调用等),为了能够专注于对该方法(单元)的逻辑进行测试,就希望能虚拟出外部依赖,避免外部依赖成为测试的阻塞项。
用到mock类框架进行虚拟这些外部依赖。
一般单元测试都是针对service层。
1.3 常用mock类框架
mock类框架:用于mock外部依赖
1.3.1 mockito
名称:ito:input to output
官网:https://site.mockito.org
官网文档:https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
限制:老版本对于final class、final method、static method、private method 均不能被 mockito mock。目前已支持final class、final method、static method 的 mock,具体可以参考官网
(39. Mocking final types, enums and final methods (Since 2.1.0))
(48. New API for mocking static methods (Since 3.4.0))
原理:bytebuddy 教程:https://www.bilibili.com/video/BV1G24y1a7bd
(通过修改字节码来实现代理的)
1.3.2 easymock
1.3.3 powermock
官网:https://github.com/powermock/powermock
与 mockito 的版本支持关系:https://gitee.com/mirrors/powermock/wikis/Mockito#supported-versions
对 mockito 或 easymock 的增强
1.3.4 JMockit
二、mockito的单独使用
2.1 mock对象与spy对象
方法插桩 | 方法不插桩 | 作用对象 | 最佳实践 | |
---|---|---|---|---|
mock对象 | 执行插桩逻辑 | 返回mock对象的默认值(0 / null / 空集合) | 类、接口 | 被测试类或其依赖 |
spy对象 | 执行插桩逻辑 | 调用真实方法 | 类、接口 | 被测试类 |
使用 mockingDetails 判断对象是否为 mock对象、spy 对象
2.2 初始化mock/spy对象的方式
方法一 | 方法二 | 方法三 | |
---|---|---|---|
junit4 | @RunWith(MockitoJUnitRunner.class) +@Mock等注解 | Mockito.mock(X.class)等静态方法 | MockitoAnnotations.openMocks(this)+@Mock等注解 |
junit5 | @ExtendWith(MockitoExtension.class) + @Mock等注解 | Mockito.mock(X.class)等静态方法 | MockitoAnnotations.openMocks(this)+@Mock等注解 |
初始化mock/spy对象第1种方式
/**
* 初始化mock/spy对象有3种方式,第1种方式
*/
@RunWith(MockitoJUnitRunner.class)
public class InitMockOrSpyMethod1Test {
@Mock
private UserService mockUserService;
@Spy
private UserService spyUserService;
@Test
public void test1() {
// true
System.out.println("Mockito.mockingDetails(mockUserService).isMock() = " + Mockito.mockingDetails(mockUserService).isMock());
// false
System.out.println("Mockito.mockingDetails(mockUserService).isSpy() = " + Mockito.mockingDetails(mockUserService).isSpy());
// true
System.out.println("Mockito.mockingDetails(spyUserService).isMock() = " + Mockito.mockingDetails(spyUserService).isMock());
// true
System.out.println("Mockito.mockingDetails(spyUserService).isSpy() = " + Mockito.mockingDetails(spyUserService).isSpy());
}
}
初始化mock/spy对象第2种方式
/**
* 初始化mock/spy对象有3种方式,第2种方式
*/
public class InitMockOrSpyMethod2Test {
private UserService mockUserService;
private UserService spyUserService;
@Before
public void init() {
mockUserService = Mockito.mock(UserService.class);
spyUserService = Mockito.spy(UserService.class);
}
@Test
public void test1() {
// true
System.out.println("Mockito.mockingDetails(mockUserService).isMock() = " + Mockito.mockingDetails(mockUserService).isMock());
// false
System.out.println("Mockito.mockingDetails(mockUserService).isSpy() = " + Mockito.mockingDetails(mockUserService).isSpy());
// true
System.out.println("Mockito.mockingDetails(spyUserService).isMock() = " + Mockito.mockingDetails(spyUserService).isMock());
// true
System.out.println("Mockito.mockingDetails(spyUserService).isSpy() = " + Mockito.mockingDetails(spyUserService).isSpy());
}
}
初始化mock/spy对象第3种方式
/**
* 初始化mock/spy对象有3种方式,第3种方式
*/
public class InitMockOrSpyMethod3Test {
@Mock
private UserService mockUserService;
@Spy
private UserService spyUserService;
@Before
public void init() {
// MockitoAnnotations.initMocks(this);
MockitoAnnotations.openMocks(this);
}
@Test
public void test1() {
// true
System.out.println("Mockito.mockingDetails(mockUserService).isMock() = " + Mockito.mockingDetails(mockUserService).isMock());
// false
System.out.println("Mockito.mockingDetails(mockUserService).isSpy() = " + Mockito.mockingDetails(mockUserService).isSpy());
// true
System.out.println("Mockito.mockingDetails(spyUserService).isMock() = " + Mockito.mockingDetails(spyUserService).isMock());
// true
System.out.println("Mockito.mockingDetails(spyUserService).isSpy() = " + Mockito.mockingDetails(spyUserService).isSpy());
}
}
2.3 参数匹配
/**
* 参数匹配:通过方法签名(参数)来指定哪些方法调用需要被处理(插桩、verify验证)
*/
@RunWith(MockitoJUnitRunner.class)
public class ParamMatcherTest {
@Mock
private UserService mockUserService;
/**
* 对于mock对象,不会调用真实方法,直接返回mock对象的默认值
* 默认值:0(int)、null、空集合
*/
@Test
public void test1() {
UserVO userVO = mockUserService.selectById(1L);
// userVO = null
System.out.println("userVO = " + userVO);
UserUpdateReq userUpdateReq = new UserUpdateReq();
int i = mockUserService.modifyById(userUpdateReq);
// i = 0
System.out.println("i = " + i);
}
/**
* 测试插桩时的参数匹配,只拦截userUpdateReq1
*/
@Test
public void test2() {
UserUpdateReq userUpdateReq1 = new UserUpdateReq();
userUpdateReq1.setId(1L);
userUpdateReq1.setPhone("1");
Mockito.doReturn(99).when(mockUserService).modifyById(userUpdateReq1);
int result1 = mockUserService.modifyById(userUpdateReq1);
// result1 = 99
System.out.println("result1 = " + result1);
UserUpdateReq userUpdateReq2 = new UserUpdateReq();
userUpdateReq2.setId(2L);
userUpdateReq2.setPhone("2");
int result2 = mockUserService.modifyById(userUpdateReq2);
// result2 = 0
System.out.println("result2 = " + result2);
int result3 = mockUserService.modifyById(userUpdateReq1);
// result3 = 99
System.out.println("result3 = " + result3);
}
/**
* 测试插桩时的参数匹配,ArgumentMatchers.any(UserUpdateReq.class)拦截UserUpdateReq类型的任意对象
* 除了ArgumentMatchers.any(XXX.class),还有anyXXX(),例如anyString(),anyLong()...
*/
@Test
public void test3() {
Mockito.doReturn(99).when(mockUserService).modifyById(ArgumentMatchers.any(UserUpdateReq.class));
UserUpdateReq userUpdateReq1 = new UserUpdateReq();
userUpdateReq1.setId(1L);
userUpdateReq1.setPhone("1");
Mockito.doReturn(99).when(mockUserService).modifyById(userUpdateReq1);
int result1 = mockUserService.modifyById(userUpdateReq1);
// result1 = 99
System.out.println("result1 = " + result1);
UserUpdateReq userUpdateReq2 = new UserUpdateReq();
userUpdateReq2.setId(2L);
userUpdateReq2.setPhone("2");
int result2 = mockUserService.modifyById(userUpdateReq2);
// result2 = 99
System.out.println("result2 = " + result2);
int result3 = mockUserService.modifyById(userUpdateReq1);
// result3 = 99
System.out.println("result3 = " + result3);
}
/**
* 测试插桩时的参数匹配,除了ArgumentMatchers.any(XXX.class),还有anyXXX(),例如anyString(),anyLong()...
* 注意:anyXXX()不包括null
*/
@Test
public void test4() {
List<String> featureList = new ArrayList<>();
featureList.add("高");
featureList.add("富");
mockUserService.add("zhangsan", "123", featureList);
mockUserService.add("wangwu", "789", featureList);
// mockUserService.add("zhangsan", "456", null);
Mockito.verify(mockUserService, Mockito.times(1)).add("zhangsan", "123", featureList);
Mockito.verify(mockUserService, Mockito.never()).add("lisi", "123", featureList);
Mockito.verify(mockUserService, Mockito.times(2)).add(ArgumentMatchers.anyString(), ArgumentMatchers.anyString(), ArgumentMatchers.anyList());
// 不允许部分使用anyXXX表达式
// Mockito.verify(mockUserService, Mockito.times(2)).add(ArgumentMatchers.anyString(), "123", featureList);
}
}
2.4 方法插桩
指定调用某个方法时的行为(stubbing),达到相互隔离的目的
返回指定值
void返回值方法插桩
插桩的两种方式
- when(obj.someMethod()).thenXxx():其中obj可以是mock对象
- doXxx().when(obj).someMethod():其中obj可以是mock/spy对象或对无返回值的方法进行插桩
抛异常
多次插桩
thenAnswer
执行真正的原始方法
verify的使用
@RunWith(MockitoJUnitRunner.class)
public class StubTest {
@Mock
private List<String> mockList;
@Mock
private UserServiceImpl mockUserServiceImpl;
@Spy
private UserServiceImpl spyUserServiceImpl;
/**
* 指定返回值
*/
@Test
public void testReturn() {
// 方法1
Mockito.doReturn("zero").when(mockList).get(0);
Assert.assertEquals("zero", mockList.get(0));
// 方法2
Mockito.when(mockList.get(1)).thenReturn("one");
Assert.assertEquals("one", mockList.get(1));
}
/**
* void返回值方法插桩
*/
@Test
public void testVoid() {
// 调用mockList.clear时候什么都不做
Mockito.doNothing().when(mockList).clear();
mockList.clear();
// 验证调用了一次clear
Mockito.verify(mockList, Mockito.times(1)).clear();
}
/**
* 插桩的两种方式
*/
@Test
public void testReturnMethod() {
Mockito.when(mockUserServiceImpl.getNumber()).thenReturn(99);
// 99
System.out.println("mockUserServiceImpl.getNumber() = " + mockUserServiceImpl.getNumber());
Mockito.when(spyUserServiceImpl.getNumber()).thenReturn(99);
// 会打印getNumber
// 再打印99
// spy对象在没有插桩时候是调用真实方法的,写在when中会导致先执行一次原方法,达不到mock的目的
System.out.println("spyUserServiceImpl.getNumber() = " + spyUserServiceImpl.getNumber());
Mockito.doReturn(999).when(spyUserServiceImpl).getNumber();
// 99
System.out.println("spyUserServiceImpl.getNumber() = " + spyUserServiceImpl.getNumber());
}
/**
* 抛出异常
*/
@Test
public void testThrowException() {
// 方法有返回值
Mockito.when(mockList.get(ArgumentMatchers.anyInt())).thenThrow(RuntimeException.class);
// Mockito.doThrow(RuntimeException.class).when(mockList).get(ArgumentMatchers.anyInt());
try {
mockList.get(4);
Assert.fail();
} catch (Exception e) {
Assert.assertTrue(e instanceof RuntimeException);
}
// 方法没返回值
Mockito.doThrow(RuntimeException.class).when(mockList).clear();
try {
mockList.clear();
Assert.fail();
} catch (Exception e) {
Assert.assertTrue(e instanceof RuntimeException);
}
}
/**
* 多次插桩
*/
@Test
public void testMultipleStub() {
Mockito.when(mockList.size()).thenReturn(1).thenReturn(2).thenReturn(3);
// Mockito.when(mockList.size()).thenReturn(1, 2, 3);
Assert.assertEquals(1, mockList.size());
Assert.assertEquals(2, mockList.size());
Assert.assertEquals(3, mockList.size());
Assert.assertEquals(3, mockList.size());
}
/**
* 指定实现逻辑的插桩
* thenAnswer可以实现对方法进行插桩,以实现自定义返回值逻辑。我们只需实现函数式接口Answer,并实现自定义返回值逻辑即可。
* 泛型表示要插桩方法的返回值类型,此处我们使用String
*/
@Test
public void testThenAnswer() {
Mockito.when(mockList.get(ArgumentMatchers.anyInt())).thenAnswer(new Answer<String>() {
@Override
public String answer(InvocationOnMock invocationOnMock) throws Throwable {
Integer argument = invocationOnMock.getArgument(0, Integer.class);
return String.valueOf(argument * 100);
}
});
String result = mockList.get(3);
Assert.assertEquals("300", result);
}
/**
* 执行真正的原始方法
*/
@Test
public void testCallRealMethod() {
// 对mock对象插桩让它执行原始方法
Mockito.when(mockUserServiceImpl.getNumber()).thenCallRealMethod();
int mockResult = mockUserServiceImpl.getNumber();
Assert.assertEquals(100, mockResult);
// 不对spy对象插桩,spy对象默认就会调用真实方法
int spyResult = spyUserServiceImpl.getNumber();
Assert.assertEquals(100, spyResult);
// 如果不想spy对象调用真实方法,则需要对它进行插桩
Mockito.doReturn(999).when(spyUserServiceImpl).getNumber();
spyResult = spyUserServiceImpl.getNumber();
Assert.assertEquals(999, spyResult);
}
/**
* 测试verify
*/
@Test
public void testVerify() {
mockList.add("one");
mockList.add("two");
mockList.clear();
Mockito.verify(mockList).add("one");
Mockito.verify(mockList, Mockito.times(1)).add("one");
Mockito.verify(mockList, Mockito.times(2)).add(ArgumentMatchers.anyString());
Mockito.verify(mockList, Mockito.never()).size();
Mockito.verify(mockList, Mockito.times(0)).size();
}
}
2.5 @InjectMocks注解的使用
- 作用:若此注解声明的变量需要用到mock/spy对象, mockito会自动使用当前类里的mock或spy成员进行按类型或名字的注入。
- 原理:构造器注入、setter注入、字段反射注入
@RunWith(MockitoJUnitRunner.class)
public class InjectMocksTest {
// 执行test1方法报错,因为被@InjectMocks注解标注的类必须是实现类,mockito会创建对应的实例对象
// @InjectMocks
// private UserService userService;
// 默认创建的对象就是未经过mockito处理的普通对象
// @InjectMocks
// private UserServiceImpl userService;
// 配合@Spy注解使其变成默认调用真实方法的对象
@InjectMocks
@Spy
private UserServiceImpl userService;
// 使用@Mock注解注入到@InjectMocks注解对应的实例对象中
// 如果不使用@Mock注解注入userFeatureService到userService中,执行test1方法过程中,userFeatureService是null
@Mock
private UserFeatureService userFeatureService;
@Test
public void test1() {
int number = userService.getNumber();
Assert.assertEquals(100, number);
}
}
断言工具
hamcrest:junit4中引入的第三方断言库,junit5中被移出,从1.3版本后,坐标由org.hamcrest:hamcrest-core变为org.hamcrest:hamcrest,用的少
assertj:常用的断言库
junit4原生断言
junit5原生断言
三、实战讲解
四、mockito在springboot环境使用(不推荐)
生成的对象受spring管理
@MockBean
- 类似@Mock
- 用于通过类型或名字替换spring容器中已经存在的bean,从而达到对这些bean进行mock的目的
@SpyBean
- 作用类似@Spy
- 用于通过类型或名字包装spring容器中已经存在的bean,当需要mock被测试类的某些方法时可以使用
附上其他代码
create database mockito_demo CHARACTER SET utf8mb4;
use mockito_demo;
drop table if exists user;
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT comment '主键',
`username` varchar(100) NOT NULL comment '用户名称',
`phone` varchar(50) NOT NULL comment '电话',
PRIMARY KEY (`id`)
) ENGINE=InnoDB comment '用户表';
drop table if exists user_feature;
CREATE TABLE `user_feature` (
`id` bigint NOT NULL AUTO_INCREMENT comment '主键',
`user_id` bigint NOT NULL comment '用户id:用户表的主键',
`feature_value` varchar(150) NOT NULL comment '用户的特征值',
PRIMARY KEY (`id`)
) ENGINE=InnoDB comment '用户特征表';
insert into user(username, phone) values('xiaoming','12345678912');
insert into user_feature(user_id,feature_value) values(1,'abc');
insert into user_feature(user_id,feature_value) values(1,'def');
insert into user_feature(user_id,feature_value) values(1,'ghi');
@SpringBootApplication
@EnableTransactionManagement
@MapperScan("com.lm.mockito.mapper")
public class MockitoApp {
public static void main(String[] args) {
SpringApplication.run(MockitoApp.class);
}
}
@RestController
@Validated
public class UserController {
@Resource
private UserService userService;
@GetMapping("/selectById")
public UserVO selectById(@NotNull Long userId) {
return userService.selectById(userId);
}
@PostMapping("/add")
public String add(@RequestBody @Validated UserAddReq addReq) {
userService.add(addReq.getUserName(), addReq.getPhone(), addReq.getFeatureValueList());
return "OK";
}
}
public interface UserService extends IService<UserDO> {
UserVO selectById(Long userId);
void add(String userName, String phone, List<String> featureValueList);
int modifyById(UserUpdateReq userUpdateReq);
int getNumber();
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserDO> implements UserService {
@Resource
private UserFeatureService userFeatureService;
@Override
public UserVO selectById(Long userId) {
UserDO existedEntity = getById(userId);
if (existedEntity == null) {
return null;
}
UserVO userVO = new UserVO();
BeanUtil.copyProperties(existedEntity, userVO);
List<UserFeatureDO> featureList = userFeatureService.selectByUserId(userId);
if (CollectionUtils.isEmpty(featureList)) {
return userVO;
}
userVO.setFeatureValueList(featureList.stream().map(UserFeatureDO::getFeatureValue).collect(Collectors.toList()));
return userVO;
}
@Override
public void add(String userName, String phone, List<String> featureValueList) {
UserDO userDO = new UserDO();
userDO.setUserName(userName);
userDO.setPhone(phone);
save(userDO);
List<UserFeatureDO> userFeatureDOList = featureValueList.stream().map(featureValue -> {
UserFeatureDO userFeatureDO = new UserFeatureDO();
userFeatureDO.setUserId(userDO.getId());
userFeatureDO.setFeatureValue(featureValue);
return userFeatureDO;
}).collect(Collectors.toList());
userFeatureService.saveBatch(userFeatureDOList);
}
@Override
public int modifyById(UserUpdateReq userUpdateReq) {
UserDO userDO = new UserDO();
userDO.setId(userUpdateReq.getId());
userDO.setUserName(userUpdateReq.getUserName());
userDO.setPhone(userUpdateReq.getPhone());
boolean successFlag = updateById(userDO);
return successFlag ? 1 : -1;
}
@Override
public int getNumber() {
System.out.println("getNumber");
return 100;
}
}
public interface UserFeatureService extends IService<UserFeatureDO> {
List<UserFeatureDO> selectByUserId(Long userId);
}
@Service
public class UserFeatureServiceImpl extends ServiceImpl<UserFeatureMapper, UserFeatureDO> implements UserFeatureService {
@Override
public List<UserFeatureDO> selectByUserId(Long userId) {
if (Objects.isNull(userId)) {
return null;
}
LambdaQueryWrapper<UserFeatureDO> lqw = Wrappers.<UserFeatureDO>lambdaQuery().eq(UserFeatureDO::getUserId,
userId);
return list(lqw);
}
}
public interface UserMapper extends BaseMapper<UserDO> {
}
public interface UserFeatureMapper extends BaseMapper<UserFeatureDO> {
}
@Data
public class UserAddReq {
@NotBlank
private String userName;
@NotBlank
private String phone;
@NotEmpty
private List<String> featureValueList;
}
@Data
public class UserUpdateReq {
@NotNull
private Long id;
@NotBlank
private String userName;
@NotBlank
private String phone;
}
@Data
public class UserVO {
private Long id;
private String userName;
private String phone;
private List<String> featureValueList;
}
@Data
@TableName("user")
public class UserDO {
@TableId(type = IdType.AUTO)
private Long id;
private String userName;
private String phone;
}
@Data
@TableName("user_feature")
public class UserFeatureDO {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String featureValue;
}
真要mock私有方法,使用powermock