Mockito协助JUnit进行测试
Mockito介绍
什么是mock测试
在写单元测试的过程中,我们往往会遇到要测试的类有很多依赖,这些依赖的类/对象/资源又有别的依赖,从而形成一个大的依赖树,要在单元测试的环境中完整地构建这样的依赖,是一件很困难的事情。
mock 测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法。这个虚拟的对象就是mock对象。mock对象就是真实对象在调试期间的代替品,方便测试独立的代码逻辑。
Mock测试框架的好处
- 可以很简单的虚拟出一个复杂对象(比如虚拟出一个接口的实现类);
- 可以配置 mock 对象的行为;
- 可以使测试用例只注重测试流程与结果;
- 减少依赖给单元测试带来的耦合。
Mock本质上是一个Proxy代理模式的应用
Mock本质上是一个Proxy代理模式的应用。如果Mockito.when传入的是一个普通对象的方法,那么只消费方法返回值是无法完成对该方法打桩的。所以Mockito本质上就是在代理对象调用方法前,用stub的方式设置其返回值,然后在真实调用时,用代理对象返回起预设的返回值。
mockito用于创建代理类(生成字节码)的工具---bytebuddy
详见: https://zhuanlan.zhihu.com/p/87523954
常见mock框架
Mockito(本文采用)、TestableMock(阿里,暂时没有正式版)、PowerMock(这个也不错)、JMockit、EasyMock
从JUnit两个注解开始
@RunWith 和 @SpringBootTest
@RunWith就是一个运行器,告诉junit的框架应该是使用哪个testRunner
@RunWith(JUnit4.class)就是指用JUnit4来运
@RunWith(Suite.class)的话就是一套测试集合
而我们常用的@RunWith(SpringRunner.class),注解的意义在于Test测试类要使用注入的类,比如@Autowired注入的类,有了@RunWith(SpringRunner.class)这些类才能实例化到spring容器中,自动注入才能生效。
详见: https://www.cnblogs.com/qingmuchuanqi48/p/11886618.html
SpringJUnit4ClassRunner和SpringRunner区别。
SpringRunner 继承了SpringJUnit4ClassRunner,没有功能扩展,相当于换了个名字。
但SpringRunner对Junit有版本要求。如果是在 4.3 之前,只能选择 SpringJUnit4ClassRunner,如果是 4.3 之后,建议选择 SpringRunner。
--------------------------------------------------------
@SpringBootTest 是在Spring Test之上的再次封装,增加了切片测试,增强了mock能力。
功能测试过程中的几个关键要素及支撑方式如下:
测试运行环境:通过@RunWith 和 @SpringBootTest启动spring容器。
mock能力:Mockito提供了强大mock功能。
断言能力:AssertJ、Hamcrest、JsonPath提供了强大的断言能力。
一旦依赖了spring-boot-starter-test,下面这些类库将被一同依赖进去:
JUnit:java测试事实上的标准,默认依赖版本是4.xx(JUnit 5和JUnit 4差别比较大,集成方式有不同)。
Spring Test & Spring Boot Test:Spring的测试支持。
AssertJ:提供了流式的断言方式。
Hamcrest:提供了丰富的matcher。
Mockito:mock框架,可以按类型创建mock对象,可以根据方法参数指定特定的响应,也支持对于mock调用过程的断言。
JSONassert:为JSON提供了断言功能。
JsonPath:为JSON提供了XPATH功能。
详见: https://www.cnblogs.com/myitnews/p/12330297.html
自定义idea的SprigbootTest模板
引导
File->Settings->Editor->File and Code Templates->"+" Name: SpringBootTest Extension: java 如何使用: 创建"Java Class"时,最下面会多一项"SpringBootTest"
模板内容
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME};#end
#parse("File Header.java")
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ${NAME} {
/**
* 初始化方法
*/
@Before
public void before(){
}
/**
* 释放资源方法
*/
@After
public void after(){
}
}
Mockito如何接入
maven方式
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.8.0</version>
<scope>test</scope>
</dependency>
(推荐)spring-boot接入 ,引入spring-boot-starter-test
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
Mockito常用操作及演示
常用注解
先看个栗子
@RunWith(SpringRunner.class)
@SpringBootTest
public class ServiceTest {
//这是一个真实的对象
@Spy
private KeySequenceService keySequenceService;
//这一个Mock对象
@Mock
private OverseasCapitalAccountMapper overseasCapitalAccountMapper;
//依赖KeySequenceService和OverseasCapitalAccountMapper
@InjectMocks
private OverseasCapitalAccountService overseasCapitalAccountService;
@Before
public void before() throws Exception {
//初始化。 会对@InjectMocks标识的对象,注入依赖。
MockitoAnnotations.initMocks(this);
//keySequenceService是真实的实例。
//我们Mock 这个方法:keySequenceService.generateSeqNo(),传入任意字符串,都返回随机数。
Mockito.doReturn(new Random().nextLong())
.when(keySequenceService)
.generateSeqNo(Mockito.anyString());
//overseasCapitalAccountService.save() 底层调用overseasCapitalAccountMapper.insert()完成存库。
//这里我们mock了overseasCapitalAccountMapper.insert()的实现
//我们定义insert()传入任何OverseasCapitalAccount对象,都应该返回 1 。
Mockito.when(overseasCapitalAccountMapper.insert()传入任何(Mockito.any(OverseasCapitalAccount.class)))
.thenReturn(1);
}
/**
* Method: generateCaNo()
*/
@Test
public void testGenerateCaNo() throws Exception {
Long caNo = keySequenceService.generateSeqNo(OverseasCapitalAccountService.key_overseas_caNo);
OverseasCapitalAccount account = new OverseasCapitalAccount();
account.setCaNo(caNo);
boolean save = overseasCapitalAccountService.save(account);
Assert.assertTrue(save);
}
}
@Mock - 用于生成模拟对象
@Spy - 把一个真实对象包装成模拟对象
@InjectMocks - 根据类型对构造方法,普通方法和字段进行依赖注入.
注意:
通过注解方式(@Mock)生成mock对象,我们必需在初始化时使用如下代码,不然即使标注@Mock注解也会是null:
MockitoAnnotations.initMocks(testClass); <-(推荐)
也可以使用: @RunWith(MockitoJUnitRunner.class)
@Mock与@Spy的区别
@Spy声明的对象,对函数的调用均执行真正部分。 通过doReturn()+when() 来配置我们需要的自定义返回的方法。 其他未配置方法,则返回真实调用结果。
@Mock声明的对象,对函数的调用均执行mock(即虚假函数),不会执行真实操作。 通过when()+thenXXX()配置我们需要的自定义返回的方法。 其他未配置方法,则返回对象默认值。
举个栗子
//-- 例
@Test
public void test1(){
//spy对象
List<Integer> realList = new LinkedList<>();
List<Integer> spyList = spy(realList);
doReturn(99).when(spyList).size();
Assert.assertEquals(99, spyList.size());
Assert.assertThrows(IndexOutOfBoundsException.class, ()->{spyList.get(0);});
try{
Integer i = spyList.get(0);
log.info("spyList.get(0): {}", i);
}catch (IndexOutOfBoundsException e){
log.error("",e);
}
//mock对象
List<Integer> mockList = mock(List.class);
when(mockList.size()).thenReturn(77);
Assert.assertEquals(77, mockList.size());
Assert.assertNull(mockList.get(0));
Assert.assertNull(mockList.get(1));
Assert.assertNull(mockList.get(55));
Assert.assertNull(mockList.get(99));
try{
Integer i = mockList.get(100);
log.info("mockList.get(100): {}", i);
}catch (IndexOutOfBoundsException e){
log.error("",e);
}
}
常用方法
mock() -虚拟对象。
功能同@Mock,但不需要初始化(见常用注解)。 如下例:
Map<String,Object> map = Mockito.mock(Map.class);
spy() -包装一个真实的对象。
功能同@Spy注解,但不需要初始化(见常用注解)。如下例:
// 创建一个LinkedList
List<String> list = new LinkedList<>();
//包装它
List<String> spyObj = spy(list);
anyXXX() -生成任意指定(或不指定)类型的对象
any()
any(Class class)
anyInt()
anyString()
anyList()
anyMap()
......
when() -可以理解为:定义 ,一般与thenXXX()或doReturn()配合使用。
thenXXX() - 一般与when配合使用。
when(操作).thenXXX(返回/抛异常/执行操作并返回) -》 定义此操作,应该返回/抛异常/执行操作并返回
几种常见then方法:
thenReturn(T obj) 返回obj
thenThrow(E exception) 抛指定(exception)异常
then(Answer a)/thenAnswer(Answer a) 执行(Answer)操作并返回
测试Controller
主要的工具类
MockMvc对象
MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用。
MockMvcBuilders类
MockMvcBuilder构造MockMvc对象。 有两个静态方法:
webAppContextSetup: 指定WebApplicationContext,将会从该上下文获取相应的控制器并得到相应的MockMvc
standaloneSetup: 通过参数指定一组控制器,这样就不需要从上下文获取了。 既可以指定Controller。
举个栗子
@RunWith(SpringRunner.class)
@SpringBootTest
public class ControllerTest {
@Autowired
WebApplicationContext context;
@Autowired
private ObjectMapper objectMapper;
// @Autowired
// private DemoController demoController;
private MockMvc mockMvc;
/**
* 初始化方法
*/
@Before
public void before() {
// //测试指定controller;可以指定多个
// mockMvc = MockMvcBuilders.standaloneSetup(demoController).build();
// 当要测试所有Controller或一个指定的url
mockMvc= MockMvcBuilders.webAppContextSetup(context).build();
}
@SneakyThrows
@Test
public void test_get(){
//DemoController.testGet()
String param_name = "param";
String param_value = "Tom";
MvcResult result= mockMvc.perform(
MockMvcRequestBuilders.get("/demo/testGet").param(param_name, param_value))
.andReturn();
//请求信息
MockHttpServletRequest request=result.getRequest();
//响应信息
MockHttpServletResponse response = result.getResponse();
String string = response.getContentAsString(CharsetUtil.CHARSET_UTF_8);
//响应: {"code":0,"msg":null,"result":"Tom"}
log.info("response : {}", string);
Map<String,Object> resMap = objectMapper.readValue(string, Map.class);
String res_param_value = (String) resMap.get("result");
Assert.assertEquals(param_value, res_param_value);
}
@SneakyThrows
@Test
public void test_saveCapitalAccount(){
//DemoController.saveCapitalAccount()
String url = "/demo/opof/capitalAccount/save";
Random random = new Random();
OverseasCapitalAccountSaveReq saveReq = new OverseasCapitalAccountSaveReq();
// saveReq.setCusSystem(3);//当传入非法参数时,会被校验器拦截,但这个case不会报错,应该对响应内容(如错误码)进行判断。
saveReq.setCusSystem(2);
saveReq.setBankAccount("bank-"+(random.nextInt(8999)+1000) );
saveReq.setChannelUid("u"+(random.nextInt(899999)+100000) );
saveReq.setCurrency("USD");
saveReq.setEcifId(String.valueOf(random.nextInt(8999)+1000) );
String jsonParam = objectMapper.writeValueAsString(saveReq);
MvcResult result = mockMvc.perform(
MockMvcRequestBuilders.post(url)
.contentType(MediaType.APPLICATION_JSON)
.characterEncoding(CharsetUtil.UTF_8)
.content(jsonParam)
).andReturn();
MockHttpServletResponse response = result.getResponse();
String string = response.getContentAsString(CharsetUtil.CHARSET_UTF_8);
log.info("response : {}", string);
}
}
资料&文档
https://site.mockito.org/ 官网
https://www.javadoc.io/doc/org.mockito/mockito-core/2.7.12/org/mockito/Mockito.html 官方文档
https://www.jianshu.com/p/7d602a9f85e3
https://segmentfault.com/a/1190000006746409
https://my.oschina.net/u/4382383/blog/3526029
https://blog.csdn.net/wwh578867817/article/details/51934404
https://www.jianshu.com/p/a07ac78a6d86
https://blog.csdn.net/dnc8371/article/details/106699739/
https://blog.csdn.net/zhuqiuhui/article/details/88602589 --@RunWith(MockitoJUnitRunner.class) vs MockitoAnnotations.initMocks(this)
https://www.jianshu.com/p/6a21edd66f4a MockMVC使用总结
https://www.cnblogs.com/jpfss/p/10950904.html MockMvc详解
https://www.cnblogs.com/xuyatao/p/8337087.html Junit测试Controller(MockMVC使用),传输@RequestBody数据解决办法
https://www.cnblogs.com/jpfss/p/10950904.html MockMvc详解
Q/A(问答)
ReflectionUtils和ReflectionTestUtils
ReflectionUtils是Spring中一个常用的类,属于spring-core包;ReflectionTestUtils则属于spring-test包。两者功能有重叠的地方,而ReflectionUtils会更强大。在单元测试时使用ReflectionTestUtils,能增加我们的便利性。
叁见: https://blog.csdn.net/wolfcode_cn/article/details/80660515