最近一直刚开始接触单元测试,也用了mockito框架,刚开始使用的话会遇到一些疑问,也会存在一些误区,所以在此记录一下使用mockito之参数匹配的问题。
1.测试类场景
首先,我们来看一下我们要测试的类,主要分为Person人员信息类,它的里面有age属性;还有一个Salary工资类,里面有Pseron属性,getSalary是根据人员的getData的返回值进行判断并返回工资的数值有没有感觉到恶意,整个类图如下图所示:
首先是Person类的代码:
public class Person {
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getData(Person person){
return person.getAge()
}
}
然后是Salary类的代码:
public class Salary {
private Person person;
public int getSalary(int age){
Person person=new Person();
person.setAge(age);
if(person.getData(person)<20){
return 1000;
}else if(person.getData(person)<40&&person.getData(person)>=20){
return 5000;
}else{
return 500;
}
}
public Person getPerson() {
return person;
}
public void setPerson(Person person) {
this.person = person;
}
}
2. 外部包引入
因为我们会使用到mockito框架,所以在maven引入依赖的包:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
3. 参数匹配测试
在现实环境中,我们调用的方法会依赖于其他类的方法,也就是其他服务,当然也包括数据库服务,在本地单元测试的时候如果想要测试完整的功能的话,难免会遇到这些依赖,当然我们可以按照真实的情况来配置这些依赖,但是启动这些服务会耗费一定,这样就违背了单元测试设计的初衷,所以我们需要使用mockito来模拟这些类,模拟这些服务。
所以在这个场景中,Salary类依赖于Person提供的服务,所以需要首先需要使用mock方法来创建Person对象:
Person person=mock(Person.class);
当然这里的Person类的创建完全可以通过构造方法,然后通过setter和getter进行设置,但是如果这个Person类的字段很多或者很复杂的情况下,mock方法能帮助我们一步到位,让我们着重于关心的方法。
模拟完对象后我们还需模拟出方法的预期,因为我们会用到Person类中的getData
这个方法,这个方法在这里我们只是简单的直接返回了age的值,实际的逻辑可能复杂,所以我们需要模拟指定调用这个方法时的返回值,使用when
方法:
when(person.getData(any())).thenReturn(19);
这个含义很明显,当我们调用person这个mock对象的值的getData
方法,参数为任意值的时候都返回19,所以相应地我们调用Salary的getSalary
方法的时候都会返回1000。
getSalary
方法的返回值与getData
方法的参数person有关,而person的值与getSalary
方法的参数age有关,那我们可不可以针对person调用getData
进行参数匹配,而不是any()
任意值呢?在这里参数匹配的意思是调用方法的时候根据参数的值来进行不同的返回。答案是可以的,参数匹配我们需要创建继承ArgumentMact,我们直接贴代码:
when(person.getData(argThat(new ArgumentMatcher<Person>() {
@Override
public boolean matches(Object o) {
if (!(o instanceof Person)) { //判断实例很重要,如果没有这一步骤有的时候莫名其妙报nullpointer异常
return false;
}
return ((Person)o).getAge()<20;
}
}))).thenReturn(19);
- 这里面核心是实现
matched
方法,只有返回true
才会满足这个匹配。 - 记得在先进行实例判断,如果传入的参数的age属性小于20则满足这个匹配。
所以想要测试完getSalary()
所以分支的情况,我们需要一共构造三种参数匹配,整体代码如下:
when(person.getData(argThat(new ArgumentMatcher<Person>() {
@Override
public boolean matches(Object o) {
if (!(o instanceof Person)) {
return false;
}
return ((Person)o).getAge()<20;
}
}))).thenReturn(19);
when(person.getData(argThat(new ArgumentMatcher<Person>() {
@Override
public boolean matches(Object o) {
if (!(o instanceof Person)) {
return false;
}
return ((Person)o).getAge()>=20&&((Person)o).getAge()<40;
}
}))).thenReturn(35);
when(person.getData(argThat(new ArgumentMatcher<Person>() {
@Override
public boolean matches(Object o) {
if (!(o instanceof Person)) {
return false;
}
return ((Person)o).getAge()>=40;
}
}))).thenReturn(60);
assertEquals(1000,salary.getSalary(10));
assertEquals(5000,salary.getSalary(30));
assertEquals(500,salary.getSalary(40));
4. 另一种简单的测试方式
getSalary
这个方法目前只有三个分支,如果分支一旦多了呢,那岂不是所有情况都要构造一个Matcher?所以在这里有一张优化方法就是直接暴力果断得设置一种情况再测试方法,这样也能和参数匹配一样有相同的覆盖率,代码如下:
when(person.getData(any())).thenReturn(19);
assertEquals(1000,salary.getSalary(10));
when(person.getData(any())).thenReturn(35);
assertEquals(5000,salary.getSalary(30));
when(person.getData(any())).thenReturn(60);
assertEquals(500,salary.getSalary(40));
这种简单的测试方法能保证覆盖率,但是却忽略了传入的
age
与getData
之间的依赖联系。
5. 总结
mockito的参数匹配功能可以让我们进行单元测试的时候尽可能的模拟真实情况,但是如果如果参数很复杂,那就需要写很多中分支,这样写单元测试的时间比本身代码还要久,这显然是不对的,所以需要我们来衡量选取哪一种方法,但是不管我们选取哪一种方法都要保证相应的覆盖率。