目录
前言
关于单元测试
什么是单元测试
为什么要写单元测试
单元测试的三部曲
Mock
什么是mock
为什么使用mock对象
MockMvc
MockMvc相关API
Mockito
Mockito相关工具
PowerMockito
PowerMockito介绍
引入PowerMockito依赖
PowerMockito的使用
PowerMockito API
PowerMockito简单实现原理
开发笔记:如何使用PowerMockito去Mock TkMybatis的Example
背景说明:
提供方案一:
提供方案二:
不写单元测试的开发不(hui)是(bei)优秀(lao'ban)的开发(zhong'chui)
鉴于我们自己对代码负责的角度(肯定不是因为领导的严格要求),我们要对单元测试有一定的了解,并且对每一段新开发的代码都要写单元测试去自测有没有问题。
这样一来减轻了测试大佬们的压力,二来对代码的质量、健壮性有所保障,三来也是最真实的:测试期少收到几个Bug
单元测试(unit testing)是指对软件中最小的可测试单元进行检查和验证。通常单元测试只测试一个方法或者一个方法调用。一个单元测试的好处:快速定位bug,提高代码质量,通过单测理解代码放心重构。单元测试代码对于代码的重构非常重要,因为一不小心犯了错,这些小范围的测试能很快作出提醒,这样就可以放心的随时调整代码。
并且在实际的开发中,单元测试会对好代码的编写有一定的帮助,譬如说所有的业务构成一段非常长的方法,在编写该方法的单元测试的时候会感觉到有点难受,有了这样的体验,开发在开发的过程中会斟酌方法是否过长,封装多几个方法会比会比较好。虽然说,可能很多小伙伴写单元测试是上级有明确指标,但是写多了也会发现它的作用,比如说在一些有很多种情况的业务中,有某种情况没考虑清楚,单元测试的时候就可能发现,这很有可能节省我们一次或多次打包发测试环境的工作。
原因:
测试驱动开发(Test Driven Development)是敏捷开发中的一种实践开发模式,其思想就是通过测试不断驱动编写完善的产品代码,实现所需的功能。
它又下面几点原则:
非能让失败的单元测试通过,否则不允许去编写任何的产品代码。
对于任何功能需求,都是先从写测试用例入手,为满足测试用例才能去写产品代码。
只允许编写刚好能够导致失败的单元测试。 (编译失败也属于一种失败)
通常在开发完成后写的测试用例都是希望能通过的测试用例,很可能因先入为主导致不能正确覆盖测试。相反,TDD编写新的测试用例是为了覆盖不同的需求,导致失败。
只允许编写刚好能够使一个失败的单元测试通过的产品代码。
编写的生产代码只能是为了使一个失败的单元测试通过,不应编写多余的实现代码。如果过多编写了实现其他功能业务的代码,则违反了TDD的原则。
它会带来哪些好处呢?
given-when-then三部曲
Given:初始化或前置条件
When:行为发生
Then:断言结果
可能从理论上看起来不好懂,我们在写单元测试的过程中就是沿着三部曲的思想去做的,这里拿一段代码举个栗子:
// GIVEN
DataSourceRequest dataSource = new DataSourceRequest();
dataSource.setPage(1);
dataSource.setPageSize(10);
when(inquiryMapper.getDemo(any())).thenReturn(newArrayList(new DemandDto()));
这段代码的具体思路就是实例化了一个自己封装的PageHelper的工具类,然后对一些属性进行赋值,这个环节就是三部曲的第一步:初始化或前置条件。然后后面借用Mockito的when方法,也就是我们在要测试的业务类中调用了inquiryMapper.getDemo()方法的地方,any()代表传入任何参数都Mock,这时候我们调用thenReturn返回一个我们初始化好的数据。when和thenReturn分别代表三部曲中的后两部:行为发生和断言结果。
汇总思路:假设我们要去单测xxx类,那么这段代码的意思就是在xxx类中调用inquiryMapper.getDemo()的时候,我们不去真实的调用而是直接返回一个我们预先初始化好的结果newArrayList(new DemandDto())。这就是Mock的体现。
在面向对象的程序设计中,模拟(mock)对象就是以可控的方式模拟真实对象的行为的假对象。在编程的过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。mock其实就是在测试过程中,对于一些不容易构造和跑通的对象和方法,通过Mock来模拟其行为。
这里讲解一下什么叫做测试过程中不容易跑通构造的方法,举一个栗子最多的情况就是Mapper中的方法,本狗子所在的项目组,对于Mapper的方法是直接Mock掉,不去实际调用数据库,其原因有几个,首先用的是使用mockito框架而不是Spring的单测,并且在不同的数据库上的数据的内容有所不同,假设开发库和测试库对不齐,那么开发库打包的时候会因为单测跑不过报错。
所以对于这种方法,我们不实际调用,当业务代码调用的时候,会直接设置我们初始化好的返回值,也就是Mock这个方法。
使用模拟对象,可以模拟复杂的、真实的对象行为。如果单元测试中无法使用真实对象,可采用模拟对象进行代替。
MockMvc是由spring-test包提供,是服务端Spring MVC测试支持的主入口。可以用来模拟客户端请求,实现了对http请求的模拟,能够直接使用网络形式,转换到controller的调用,不依赖网络环境。同时提供一套验证的工具,使得结果验证比较方便。
使用spring-test编写MockMvc单元测试需要在类上加上:@RunWith(SpringRunner.class)和@SpringBootTest注解,也可以加上@AutoConfigreMockMvc 来注入MockMvc
@RunWith用于指定测试运行器,如果不声明具体的运行器,将使用默认的运行器BlockJUnit4ClassRunner。@WebAppConfiguration注解用于声明测试时所加载的是webApplicationContext,但是它需要结合@ContextConfiguration注解一起使用。表示测试环境使用的ApplicationContext将是WebApplicationContext类型的。
同时也可以用@Before来进行初始化,@Before会执行在@Test之前
示例:
@Before
public void setUp() throws Exception {
// 实例化方式一
mockMvc = MockMvcBuilders.standaloneSetup(new XxController()).build();
// 实例化方式二
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
//Mockito实例化方式
MockitoAnnotations.initMocks(this);
}
Perform():用于执行请求,通过post或者get传入相关请求
AndDo():执行普通处理,使用print()方法用于打印请求或者相应及其他信息
AndExcept():执行预期匹配,如status().ok()表示预期相应成功,或content().string()表示预期的返回结果值
andReturn():用于返回请求访问对象MvcRsult
param()和content()用法相识,表示添加请求后传值
示例:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DemandApplication.class})
@AutoConfigureMockMvc
public class XxControllerTest {
@Autowired
IXxService xxService;
MockMvc mockMvc;
@Before
public void setUp() throws Exception {
// 实例化方式一
mockMvc = MockMvcBuilders.standaloneSetup(new XxController()).build();
// 实例化方式二
// mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
//Mockito实例化方式
//MockitoAnnotations.initMocks(this);
}
@Test
public void closeTest() throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("headerId", "60000");
String requestJson = JSONObject.toJSONString(jsonObject);
mockMvc.perform(post("/xx/xx/close").contentType(MediaType.APPLICATION_JSON).content(requestJson))
.andDo(print())
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
}
@Test
public void cloneTest() throws Exception {
JSONObject jsonObject = new JSONObject();
jsonObject.put("headerId", "50000");
String requestJson = JSONObject.toJSONString(jsonObject);
mockMvc.perform(post("/xx/xx/cancel").contentType(MediaType.APPLICATION_JSON).content(requestJson))
.andDo(print()).andExpect(status()
.isOk()).andReturn().getResponse().getContentAsString();
}
}
Mockito工具可以和Junit工具组合使用,mockito本质上是一个proxy代理模式的应用。是在代理对象调用方法前,用stub方式设置其返回值,然后在真实调用时,用代理对象返回起预设的返回值。使用Mockito一般分为三个步骤:
使用mockito测试框架需要使用@RunWith(MockitoJUnitRunner.class)注解来声明测试运行器。然后使用@Before来进行初始化,另外使用mock一个对象,对象为null。使用初始化之后就不会抛出空指针异常。
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
Mockito测试框架主要用来测试Service的代码,需要用@Mock来模拟DAO层操作,然后使用@InjectMock来注入我们需要测试的service层。@Mock标注的对象会自动被注入到被@InjectMock标注的对象中。同时对于@mock标注的对象,mockito还提供了参数匹配器argument matchers,例如使用anyString匹配任何String参数,使用anyInt()匹配任何int参数,anySet()匹配任何set集合,对于用户自定义的类型,可以使用argThat()方法来匹配自定义类型。甚至使用any()可以代替任何类型的参数,any()也可以传入某个类来做特殊指定。另外有一点需要注意的是,如果一个方法有多个参数,如果一个参数使用了参数匹配器,其余参数必须都使用参数匹配器。
此外创建mock对象不能对final,Anonymous,static类进行mock。当我们使用mockito时,使用静态导入可以使代码更简洁,如
import static org.mockito.Mockito.*;
Mockito常用方法:
When(Object):为函数进行打桩(即stub,是指完全模拟一个外部依赖),而mock用来判断测试是通过还是失败
Verify():函数可以验证函数的调用次数,注意verify传入的参数必须是一个mock对象
DoThrow():用于对无返回值函数打桩进行异常抛出
Donothing():用于对无返回值函数进行测试
DoReturn():用于自定义函数的返回值
Spy():使用spy()所生成的类,所有方法都是真实方法,fan返回值都是和真实方法是一样的。而使用Mock生成的类,所有方法都不是真实方法,返回值都是null。DocallRealMethod()方法可以获取方法真实的返回值。
Mockito单元测试示例:
@RunWith(JUnit4.class)
public class PageHelpTest {
@InjectMocks
private InquiryServiceImpl inquiryService;
@Mock
private InquiryMapper inquiryMapper;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
/**
* 分页测试
*/
@Test
public void pageTest() {
// GIVEN
PageDataSourceRequest dataSource = new PageDataSourceRequest();
dataSource.setPage(2);
dataSource.setPageSize(5);
dataSource.handleUnknown("name", "test");
when(inquiryMapper.getDemo(any())).thenReturn(new ArrayList(new DemandDto()));
// WHEN
PageInfo page = inquiryService.testPageHelp(new PageParams(dataSource.getPage(),
dataSource.getPageSize()), dataSource.toObject(DemandDto.class));
// THEN
Assert.assertEquals(1, page.getPageSize());
}
}
Mockito API:https://static.javadoc.io/org.mockito/mockito-core/3.0.0/org/mockito/Mockito.html
在写单元测试的时候,我们会发现我们要测试的方法会引入很多外部依赖对象(比如外部邮件服务、网络通讯和远程微服务调用等),由于我们我所依赖的服务不由我所在的项目组维护(对方接口可能中途会发生变化,甚至,有时候可能并未启动)。集成测试成本略高,故而需要mock工具来模拟这些外部依赖的对象,来完成单元测试。
为什么要引入PowerMockito呢?现如今比较流行的mock工具如Jmock、EasyMock、Mockito等都有一个共同缺陷:不能mock静态、final、private等。而PowerMockito提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMockito现对静态方法、构造方法、私有方法以及final方法的模拟支持,对静态初始化过程的移除等强大的功能。
下面是PowerMockito的官方介绍:
从官方介绍中,我们可以看出PowerMockito使用了Mockito API,我们可以同时使用两个mock框架进行单元测试,PowerMockito 2.0.0以及更高的版本具有Mockito 2的支持,1.7.0及更高的版本具有Mockito 2试用版的支持。PowerMockito旨在用少量的方法和注解扩展现有的API来实现额外的功能。目前PowerMockito支持EasyMock和Mockito。
org.mockito
mockito-core
2.23.0
org.apache.commons
commons-collections4
org.powermock
powermock-module-junit4
RELEASE
test
org.powermock
powermock-api-mockito2
2.0.2
test
PowerMockito有两个重要的注解:
如果你的单元测试李没有使用注解@PrepareForTest,那么可以不用加注解@RunWith(PowerMockRunner.class)。反之,如果当你要使用PowerMockito强大功能(mock静态、final、private等方法时)时,就需要加注解@PrepareForTest,注意花括号{}里面可以创建多个类的实例,用逗号隔开。如
@PrepareForTest({ YourClass.class,MyClass.class })
当我们要mock一个静态方法时,有三种方法去使用,第一种是使用@PrepareForTest注解创建一个静态类实例。
@PrepareTest(Static.class)
第二种使我们需要调用PowerMockito.mockStatic()去mock一个静态类,如
PowerMockito.mockStatic(Static.class);
第三种是使用when并模拟返回值
Mockito.when(Static.firstStaticMethod(param)).thenReturn(value);
当我们需要去验证静态方法的行为时,需要使用PowerMockito.verifyStatic(Static.class)去开启验证,然后直接去调用静态方法即可。如
PowerMockito.verifyStatic(Static.class);
Static.firstStaticMethod(param);
PowerMockito参数匹配器的用法和Mockito类似,这里不再赘述,请看看下列例子
PowerMockito.verifyStatic(Static.class);
Static.thirdStaticMethod(Mockito.anyInt());
同时使用verify的使用方法也和Mockito的类似,我们可以使用verifyPrivate去验证行为,verifyPrivate也可以去mock一个private static方法
Mockito.verify(mockObj, times(2)).methodToMock();
verifyPrivate(tested).invoke("privateMethodName", argument1);
使用doThrow去mock一个void方法
PowerMockito.doThrow(new ArrayStoreException("Mock error")).when(StaticService.class);
StaticService.executeMethod();
使用PowerMockito.verifyPrivate去验证new Object的构造器
verifyNew(MyClass.class).withNoArguments();
使用spy去模拟真实调用:
PartialMockClass classUnderTest = PowerMockito.spy(new PartialMockClass());
Mockito.when(classUnderTest.methodToMock()).thenReturn(value);
classUnderTest.execute();
Mockito.verify(mockObj, times(2)).methodToMock();
https://static.javadoc.io/org.powermock/powermock-api-mockito/1.6.2/org/powermock/api/mockito/PowerMockito.html
当某个测试方法被注解@PrepareForTest标注以后,在运行测试用例时,会创建一个新的org.powermock.core.classloader.MockClassLoader实例,然后加载该测试用例使用到的类(系统类除外)。PowerMockito会根据你的mock要求,去修改@PrepareForTest里的class文件(当测试类会自动加入注解中),以满足特殊的mock需求。例如:去除final方法的final标识,在静态方法的最前面加入自己的虚拟实现等。如果需要mock的是系统类的final方法和静态方法,PowerMockito不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。关于更多实现源码请参考:https://github.com/powermock/powermock。
我们在写单测的时候,如果去mock example的查询的话会报找不到表的错误,就很迷,于是产生了几种思路:1. 不用example。 2. 单元测试跳过example的地方。这两种思路当然都不可以,如果因为单测而不用example,那么我觉得是给单测束缚住了,这样和初衷不符,如果跳过example的地方的话,那跳过的就太多了,单测覆盖率也低。
@PrepareForTest({XxServiceImpl.class})
说明:XxServiceImpl是自己需要去单测的目标类
缺陷:这种方法虽然可以直接mock Example的方法,但是如果部门使用sonar来做单元测试覆盖率扫描的话这种方法是扫描不到覆盖率的,对于覆盖率很看重的当然需要摒弃这种做法。
写了较久的单测,觉得对于Example这个坑是肯定要过的,而且肯定要扫描到覆盖率。
所以下面将从找不到表结构开始进行分析如何深入Mock
分析过程
解决找不到表结构问题:
通过ctrl+鼠标左键进去源码查看如何去new一个Example实例的过程:
解决方案:
mock调这个方法
//头上记得加@PrepareForTest({EntityTable.class})
//没有表结构就给它表结构
EntityTable table = PowerMockito.mock(EntityTable.class);
PowerMockito.when(EntityHelper.getEntityTable(Mockito.any())).thenReturn(table);
criteria.andEqualTo()找不到属性名问题解决:
去看criteria内部的源码报错的地方通过自己Mock代替它的实例化
//头上记得加@PrepareForTest({Example.class})
Example.Criteria criteria = PowerMockito.mock(Example.Criteria.class);
//这里为了将目标业务层中的criteria替换成我们自己mock的实例方便后面mock
PowerMockito.whenNew(Example.Criteria.class).withArguments(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean()).thenReturn(criteria);
最终解决:(头上记得加@PrepareForTest({Example.class, EntityTable.class}))【EntityTable是Example内部代码调用的类】
//mock example
PowerMockito.mockStatic(EntityHelper.class);
//没有表结构就给它表结构
EntityTable table = PowerMockito.mock(EntityTable.class);
PowerMockito.when(EntityHelper.getEntityTable(Mockito.any())).thenReturn(table);
Example.Criteria criteria = PowerMockito.mock(Example.Criteria.class);
//这里为了将目标业务层中的criteria替换成我们自己mock的实例方便后面mock
PowerMockito.whenNew(Example.Criteria.class).withArguments(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean()).thenReturn(criteria);
//模拟example查询
PowerMockito.when(criteria.andEqualTo(Mockito.anyString(), Mockito.any())).thenReturn(criteria);
转载注明出处。