记录一下,工作中编写单元测试的常用方法。
参数化测试
参数化是自动化测试的一种常用技巧,可以将测试代码中的某些输入使用参数来代替。
一、准备一个被测试类
public class Sample {
public int add(int numA, int numB) {
return numA + numB;
}
}
二、生成对应的测试类
public class SampleTest {
private Sample sample;
@Before
public void setUp() throws Exception {
sample = new Sample();
}
@Test
public void add() {
assertEquals(3, sample.add(1, 2));
}
}
三、测试用例编写
我们可能会这样编写:
@Test
public void add() {
assertEquals(3, sample.add(1, 2));
assertEquals(4, sample.add(2, 2));
assertEquals(5, sample.add(3, 2));
assertNotEquals(6, sample.add(1, 2));
}
这时候问题就出现了,以上写法的问题是这些测试用例不是纯粹的“单元",而是变成系列了,有先后关系了。整个测试过程只执行了一次 setUp()
操作,所以说每个测试用例的环境是不一样的。
那么如何做到纯粹的单元测试呢?那就是每个方法每次执行只测一个用例,然后就执行 tearDown()
,当然我们不会每次执行完之后再去修改assertEquals(3, sample.add(1, 2))
中的参数,这样效率太低。
这个时候我们就可以通过参数化标注的手段来实现多组纯粹的单元测试。。
四、参数化测试
首先在测试类上添加注解 @RunWith(Parameterized.class)
,在创建一个由 @Parameters
注解的 public static
方法,让返回一个对应的测试数据集合。最后创建构造方法,方法的参数顺序和类型与测试数据集合一一对应。
@RunWith(Parameterized.class)
public class SampleTest {
private Sample sample;
int expected = 0;
int input1 = 0;
int input2 = 0;
@Parameterized.Parameters
public static java.util.Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{3, 1, 2},
{4, 2, 2},
{0, -2, 2}
});
}
public SampleTest(int expected, int input1, int input2) {
this.expected = expected;
this.input1 = input1;
this.input2 = input2;
}
@Before
public void setUp() throws Exception {
sample = new Sample();
}
@Test
public void add() throws Exception {
assertEquals(expected, sample.add(input1, input2));
}
}
通过参数化测试,可以保证测试方法每次执行的之后只会测试一个测试用例,测试完成之后执行 tearDown()
做清理工作,保证每个测试用例不会相互影响。
实现的逻辑是参数化运行器每次读取一组数据的时候都会重新通过传入数据组给测试类构造器,从而创建一个新对象,然后执行 setUp()、“testYourMethod()”、tearDown()
操作。
Mockito
Mockito
是一个流行的开源框架,用于在软件测试中模拟对象。使用 Mockito
大大简化了具有外部依赖关系的类的测试开发。
模拟对象是接口或类的虚拟实现。它允许定义某些方法调用的输出。在测试环境中,用来替换掉真实的对象,以达到两大目的:
- 验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等。
- 指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作。
引入依赖及版本:
mockitoVersion=3.12.4
testImplementation ("org.mockito:mockito-core:$rootProject.ext.mockitoVersion")
testImplementation ("org.mockito:mockito-inline:$rootProject.ext.mockitoVersion")
Mockito
提供了几种创建模拟对象的方法:
1. 将 JUnit 5 的 @ExtendWith(MockitoExtension.class)扩展与字段上的 @Mock 注释结合使用(目前使用的是 JUnit4,暂忽略)
2. 使用静态mock()方法。
3. 使用@Mock注释。
好了,下面来看常用方法:
一、Mock 静态方法
@Test
fun getBtnColor() {
Mockito.mockStatic(StatuesUtil::class.java).use {
`when`(StatuesUtil.getSystemSwitchStatues("type1")).thenReturn(true)
Assert.assertEquals(
presenter.getBtnColor("type1"),
R.color.display_red1
)
}
}
注:这里 getBtnColor() 中有用到 StatuesUtil 里面的静态方法,所以这里通过 Mock 做了一个挡板,
当运行到 `StatuesUtil.getSystemSwitchStatues("type1")`,就会直接返回 true。
如果参数化测试的时候,需要 Mock
静态方法这时就需要在用完去关闭,代码如下:
...
val mockitoStatuesUtil = Mockito.mockStatic(StatuesUtil::class.java)
`when`(StatuesUtil.getScreenData()).thenReturn(1)
...
mockitoStatuesUtil.close()
...
普通类的 final
方法,方式同静态方法。
二、Mock 单例类
有两种形式:
1. 单例类里面
val mocked:DisplayCache = mock(DisplayCache::class.java)
`when`(mocked.source).thenReturn(create().dataIntegrationList)
注:如果在单例类 DisplayCache 里面,可直接通过 mock 这种形式,直接挡板,返回模拟数据。
2. 其他类中(非 DisplayCache 类中)
...
val displayCache: DisplayCache = DisplayCache.getInstance()
val spy: DisplayCache = Mockito.spy(displayCache)
spy.addAll(params3, "系统名")
...
注: addAll() 为单例类里面的方法。
区别:spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值。
三、Mock 私有方法
通过反射验证 DisplayTemporaryData 类中的私有方法 screenData()
,代码如下:
...
val displayTemporaryData: DisplayTemporaryData = DisplayTemporaryData.getInstance()
val spy: DisplayTemporaryData = Mockito.spy(displayTemporaryData)
val method: Method = DisplayTemporaryData::class.java.getDeclaredMethod(
"screenData",
MutableList::class.java, String::class.java, Boolean::class.java
)
method.isAccessible = true
method.invoke(spy, list, params2, params3)
...
注:私有方法对于测试是不可见的,它们一般也不能被 mock(如果不是通过反射)。
当然这里也可通过 PowerMockito
方式,代码如下:
public class ClassUnderTest {
public boolean callPrivateMethod() {
return isExist();
}
private boolean isExist() {
return false;
}
}
@RunWith(PowerMockRunner.class)
public class TestClassUnderTest {
@Test
@PrepareForTest(ClassUnderTest.class)
public void testCallPrivateMethod() throws Exception {
ClassUnderTest underTest = PowerMockito.mock(ClassUnderTest.class);
PowerMockito.when(underTest.callPrivateMethod()).thenCallRealMethod();
PowerMockito.when(underTest, "isExist").thenReturn(true);
Assert.assertTrue(underTest.callPrivateMethod());
}
}
注:用 `PowerMockito` 还需要添加依赖,而且
四、其他常用方法
单元测试的目的是为了测试我们自己写的代码的正确性,它不需要测试外部的各种依赖,所以当我们遇到一个方法中有很多别的对象的依赖的时候,比如操作数据库,连接网络,读写文件等等,需要给它解依赖。
怎么解依赖呢?其实就是弄一些假对象,比如代码中是我们从网络获取一段json数据,转化成一个对象传入到我们的测试方法中。那么就可以直接 new
一个假的对象,并给它设置我们期望的返回值传给要测试的方法就好了,不需要再去请求网络获取数据。这个过程称之为 mock
。
Mockito
中存在着两种类型:mock
和 spy
。
1. 结果验证
测试某些结果是否正确,使用 when
和 thenReturn
可根据传递给方法的参数,mock 可以返回不同的值。最后通过 assertEquals
判断返回值是否正确,代码如下:
@Test
public void testMockitoResult() {
Person person = mock(Person.class);
//当调用person.getAge()方法的时候,给它返回一个18
when(person.getAge()).thenReturn(18);
//当调用person.getName()方法的时候,给它返回一个Lily
when(person.getName()).thenReturn("Lily");
//判断返回跟预期是否一样
assertEquals(18, person.getAge());
assertEquals("Lily", person.getName());
}
2. 验证行为
有时候会测试某些行为是否被执行过,通过 verify
方法可以验证某个方法是否执行过,执行的次数:
@Test
public void testMockitoBehavior() {
Person person = mock(Person.class);
int age = person.getAge();
//验证getAge动作有没有发生
verify(person).getAge();
//验证person.getName()是不是没有调用
verify(person, never()).getName();
//验证是否最少调用过一次person.getAge
verify(person, atLeast(1)).getAge();
//验证getAge动作是否被调用了2次,前面只用了一次所以这里会报错
verify(person, times(2)).getAge();
}
3. mock 默认值
通过 Mockito mock
一个 Person
对象,那么这个对象的 name
属性是默认为 null
的,如果我们不想让它为 null
,默认为空字符串可以使用 RETURNS_SMART_NULLS
:
@Test
public void testNotNull(){
Person person = mock(Person.class);
System.out.println(person.getName());
Person person1 = mock(Person.class,RETURNS_SMART_NULLS);
System.out.println(person1.getName());
}
4. @Mock注解
使用 @Mock
注解来 mock
一个对象:
@Mock
List<Integer> mList;
@Test
public void testAnnotationMock(){
mList.add(0);
verify(mList).add(0);
}
5. 验证是否执行了某个参数的方法
@Test
public void testParameter(){
Person person = mock(Person.class);
when(person.getDuty(1)).thenReturn("医生");
System.out.println(person.getDuty(1));
//anyInt任何Int值,此外还有anyString,anyFloat等
when(person.getDuty(anyInt())).thenReturn("护士");
System.out.println(person.getDuty(anyInt()));
//验证person.getDuty(1)方法有没有调用
verify(person).getDuty(ArgumentMatchers.eq(1));
}
6. mock 真实对象
mock
出来的对象都是虚拟的对象,我们可以验证其执行次数,状态等,如果一个对象是真实的,那怎么验证呢 可以使用 spy
包装一下。
spy
对象的方法默认调用真实的逻辑,mock
对象的方法默认什么都不做,或直接返回默认值,代码如下:
@Test
public void testSpy(){
Person person = getPerson();
Person spy = spy(person);
when(spy.getName()).thenReturn("Lily");
System.out.println(spy.getName());
verify(spy).getName();
}
private Person getPerson(){
return new Person();
}