1. 单元测试介绍
单元测试是应用程序测试策略中的基本测试,通过对代码进行单元测试,可以轻松地验证单个单元的逻辑是否正确,在每次构建之后运行单元测试,可以帮助您快速捕获和修复因代码更改(重构、优化等)带来的回归问题。本文主要聊聊Android中的单元测试
1.1为什么要进行单元测试?
提高稳定性,能够明确地了解是否正确的完成开发;
快速反馈bug,跑一遍单元测试用例,定位bug;
在开发周期中尽早通过单元测试检查bug,最小化技术债,越往后可能修复bug的代价会越大,严重的情况下会影响项目进度;
为代码重构提供安全保障,在优化代码时不用担心回归问题,在重构后跑一遍测试用例,没通过说明重构可能是有问题的,更加易于维护。
1.2单元测试要测什么?
- 列出想要测试覆盖的正常、异常情况,进行测试验证;
- 性能测试,例如某个算法的耗时等等。
1.3单元测试的分类
-
本地测试(Local tests): 只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如google推荐的[Mockito][1];
-
仪器化测试(Instrumented tests): 在真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式。
2.Junit注解介绍
JUnit是Java最基础的测试框架,主要的作用就是断言。使用时在app的build文件中添加依赖。
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
Assert类中主要方法如下:
方法名 | 方法描述 |
---|---|
assertEquals | 断言传入的预期值与实际值是相等的 |
assertNotEquals | 断言传入的预期值与实际值是不相等的 |
assertArrayEquals | 断言传入的预期数组与实际数组是相等的 |
assertNull | 断言传入的对象是为空 |
assertNotNull | 断言传入的对象是不为空 |
assertTrue | 断言条件为真 |
assertFalse | 断言条件为假 |
assertSame | 断言两个对象引用同一个对象,相当于“==” |
assertNotSame | 断言两个对象引用不同的对象,相当于“!=” |
assertThat | 断言实际值是否满足指定的条件 |
注意:上面的每一个方法,都有对应的重载方法,可以在前面加一个String类型的参数,表示如果断言失败时的提示。
JUnit 中的常用注解:
注解名 | 含义 |
---|---|
@Test | 表示此方法为测试方法 |
@Before | 在每个测试方法前执行,可做初始化操作 |
@After | 在每个测试方法后执行,可做释放资源操作 |
@Ignore | 忽略的测试方法 |
@BeforeClass | 在类中所有方法前运行。此注解修饰的方法必须是static void |
@AfterClass | 在类中最后运行。此注解修饰的方法必须是static void |
@RunWith | 指定该测试类使用某个运行器 |
@Parameters | 指定测试类的测试数据集合 |
@Rule | 重新制定测试类中方法的行为 |
@FixMethodOrder | 指定测试类中方法的执行顺序 |
执行顺序:@BeforeClass –> @Before –> @Test –> @After –> @AfterClass
assertThat用法
上面我们所用到的一些基本的断言,如果我们没有设置失败时的输出信息,那么在断言失败时只会抛出AssertionError,无法知道到底是哪一部分出错。而assertThat就帮我们解决了这一点。它的可读性更好。
assertThat(T actual, Matcher<? super T> matcher);
assertThat(String reason, T actual, Matcher<? super T> matcher);
其中reason为断言失败时的输出信息,actual为断言的值,matcher为断言的匹配器。
常用的匹配器整理:
匹配器 | 说明例子 |
---|---|
is | 断言参数等于后面给出的匹配表达式 assertThat(5, is (5)); |
not | 断言参数不等于后面给出的匹配表达式 assertThat(5, not(6)); |
equalTo | 断言参数相等 assertThat(30, equalTo(30)); |
equalToIgnoringCase | 断言字符串相等忽略大小写 assertThat(“Ab”, equalToIgnoringCase(“ab”)); |
containsString | 断言字符串包含某字符串 assertThat(“abc”, containsString(“bc”)); |
startsWith | 断言字符串以某字符串开始 assertThat(“abc”, startsWith(“a”)); |
endsWith | 断言字符串以某字符串结束 assertThat(“abc”, endsWith(“c”)); |
nullValue | 断言参数的值为null assertThat(null, nullValue()); |
notNullValue | 断言参数的值不为null assertThat(“abc”, notNullValue()); |
greaterThan | 断言参数大于 assertThat(4, greaterThan(3)); |
lessThan | 断言参数小于 assertThat(4, lessThan(6)); |
greaterThanOrEqualTo | 断言参数大于等于 assertThat(4, greaterThanOrEqualTo(3)); |
lessThanOrEqualTo | 断言参数小于等于 assertThat(4, lessThanOrEqualTo(6)); |
closeTo | 断言浮点型数在某一范围内 assertThat(4.0, closeTo(2.6, 4.3)); |
allOf | 断言符合所有条件,相当于&& assertThat(4,allOf(greaterThan(3), lessThan(6))); |
anyOf | 断言符合某一条件,相当于或 assertThat(4,anyOf(greaterThan(9), lessThan(6))); |
hasKey | 断言Map集合含有此键 assertThat(map, hasKey(“key”)); |
hasValue | 断言Map集合含有此值 assertThat(map, hasValue(value)); |
hasItem | 断言迭代对象含有此元素 assertThat(list, hasItem(element)); |
注意:assertThat()是Assert类中的静态方法现在已经被废弃,推荐使用 MatcherAssert.assertThat()
JUnit 中@Rule注解介绍
利用 @Rule 我们可以扩展 Junit 的功能,在执行case的时候加入测试者特有的操作,而不影响原有case的代码,减少了特有操作和test case原逻辑的耦合。
@Rule 只能注解在字段中,该字段必须是 public 的并且类型必须实现了 TestRule 接口或者 MethodRule 接口。
Junit 4.9之后还加入了一个 @ClassRule 注解。相对 @Rule 来说, @ClassRule 是一个类级别的注解。就像 @Before 与 @BeforeClass 的区别。
下面是Junit自带的实现TestRule接口的类
//生成临时文件或临时文件夹 必须是public修饰
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
//在一个运行测试方法过程中收集多个错误信息
@Rule
public ErrorCollector errorCollector = new ErrorCollector();
//设置执行最长时间
@Rule
public Timeout timeout = new Timeout(1000, TimeUnit.MILLISECONDS);
//取得当前的测试方法名称
@Rule
public TestName testName = new TestName();
以上类的用法示例,比如打印当前方法名:
@Rule
public TestName testName = new TestName();
@Test
public void isEmail_1() {
System.out.println("方法名称 = "+testName.getMethodName());
TestUtil testUtil = new TestUtil();
boolean result = testUtil.isEmail("123");
Assert.assertEquals(false,result);
}
当然我们也可以实现自定义的Rule
public class MyRule implements TestRule {
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
// evaluate前执行方法相当于@Before
String methodName = description.getMethodName(); // 获取测试方法的名字
System.out.println(methodName + "测试开始!");
base.evaluate(); // 运行的测试方法
// evaluate后执行方法相当于@After
System.out.println(methodName + "测试结束!");
}
};
}
}
//自定义Rule打印方法开始执行和方法执行结束
@Rule
public MyRule myRule = new MyRule();
@Test
public void isEmail_1() {
System.out.println("方法名称 = "+testName.getMethodName());
TestUtil testUtil = new TestUtil();
boolean result = testUtil.isEmail("123");
Assert.assertEquals(false,result);
}
执行测试方法结果如下图:
![](https://img.haomeiwen.com/i16714432/7bb870707bffe82f.png)
使用RuleChain
RuleChain提供一种将多个TestRule串在一起执行的机制。这在JUnit 4.10以后的版本中可以使用。需要根据特定顺序执行多个处理的时候,用RuleChain可以提高效率
public class MyRule2 implements TestRule {
public String str = "";
public MyRule2(String str) {
this.str = str;
}
@NonNull
@Override
public Statement apply(@NonNull Statement base, @NonNull Description description) {
System.out.println(str);
return new Statement() {
@Override
public void evaluate() throws Throwable {
base.evaluate();
}
};
}
}
@Rule
public TestRule testRule = RuleChain.outerRule(new MyRule2("----------111"))
.around(new MyRule2("----------222"))
.around(new MyRule2("----------333"));
@Test
public void isEmail_2() {
TestUtil testUtil = new TestUtil();
boolean result = testUtil.isEmail("abc@163.com");
Assert.assertEquals(true,result);
}
![](https://img.haomeiwen.com/i16714432/b33d9004099b8ae0.png)
3.Mockito介绍
使用时在build文件中添加依赖。
dependencies {
testImplementation "org.mockito:mockito-core:3.3.3"
androidTestImplementation 'org.mockito:mockito-android:3.3.3'
}
Mockito创建对象的方式
- 普通方法
public class MockitoTest {
@Test
public void testIsNotNull(){
Person mPerson = mock(Person.class); //<--使用mock方法
assertNotNull(mPerson);
}
}
- 注解方法
public class MockitoAnnotationsTest {
@Mock //<--使用@Mock注解
Person mPerson;
@Before
public void setup(){
MockitoAnnotations.initMocks(this); //<--初始化
}
@Test
public void testIsNotNull(){
assertNotNull(mPerson);
}
}
- 运行器方法:
@RunWith(MockitoJUnitRunner.class) //<--使用MockitoJUnitRunner
public class MockitoJUnitRunnerTest {
@Mock //<--使用@Mock注解
Person mPerson;
@Test
public void testIsNotNull(){
assertNotNull(mPerson);
}
- MockitoRule方法
public class MockitoRuleTest {
@Mock //<--使用@Mock注解
Person mPerson;
@Rule //<--使用@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void testIsNotNull(){
assertNotNull(mPerson);
}
}
常用打桩方法
因为Mock出的对象中非void方法都将返回默认值,比如int方法将返回0,对象方法将返回null等,而void方法将什么都不做。“打桩”顾名思义就是将我们Mock出的对象进行操作,比如提供模拟的返回值等,给Mock打基础。
方法名 | 方法描述 |
---|---|
thenReturn(T value) | 设置要返回的值 |
thenThrow(Throwable… throwables) | 设置要抛出的异常 |
thenAnswer(Answer<?> answer) | 对结果进行拦截 |
doReturn(Object toBeReturned) | 提前设置要返回的值 |
doThrow(Throwable… toBeThrown) | 提前设置要抛出的异常 |
doAnswer(Answer answer) | 提前对结果进行拦截 |
doCallRealMethod() | 调用某一个方法的真实实现 |
doNothing() | 设置void方法什么也不做 |
关于Mockito.when(Method()).thenReturn(...) 与 Mockito.doReturn(...).when(...).Method()前者会执行到方法里,然后在将返回值Mock掉。 后者不会走到Methond()里,提前拦截直接返回,常用在void方法。
常用验证方法
前面所说的都是状态测试,但是如果不关心返回结果,而是关心方法有否被正确的参数调用过,这时候就应该使用验证方法了。从概念上讲,就是和状态测试所不同的“行为测试”了。
verify(T mock)
验证发生的某些行为 。
方法名 | 方法描述 |
---|---|
after(long millis) | 在给定的时间后进行验证 |
timeout(long millis) | 验证方法执行是否超时 |
atLeast(int minNumberOfInvocations) | 至少进行n次验证 |
atMost(int maxNumberOfInvocations) | 至多进行n次验证 |
description(String description) | 验证失败时输出的内容 |
times(int wantedNumberOfInvocations) | 验证调用方法的次数 |
never() | 验证交互没有发生,相当于times(0) |
only() | 验证方法只被调用一次,相当于times(1) |
常用参数匹配器
方法名 | 方法描述 |
---|---|
anyObject() | 匹配任何对象 |
any(Class<T> type) | 与anyObject()一样 |
any() | 与anyObject()一样 |
anyBoolean() | 匹配任何boolean和非空Boolean |
anyByte() | 匹配任何byte和非空Byte |
anyCollection() | 匹配任何非空Collection |
anyDouble() | 匹配任何double和非空Double |
anyFloat() | 匹配任何float和非空Float |
anyInt() | 匹配任何int和非空Integer |
anyList() | 匹配任何非空List |
anyLong() | 匹配任何long和非空Long |
anyMap() | 匹配任何非空Map |
anyString() | 匹配任何非空String |
contains(String substring) | 参数包含给定的substring字符串 |
argThat(ArgumentMatcher <T> matcher) | 创建自定义的参数匹配模式 |
其他方法
方法名 | 方法描述 |
---|---|
reset(T … mocks) | 重置Mock |
spy(Class<T> classToSpy) | 实现调用真实对象的实现 |
inOrder(Object… mocks) | 验证执行顺序 |
@InjectMocks注解 | 自动将模拟对象注入到被测试对象中 |
Mock 和 Spy 区别:
如果不指定的话,一个mock对象的所有非void方法都将返回默认值:int、long类型方法将返回0,boolean方法将返回false,对象方法将返回null等等;而void方法将什么都不做。
总之,spy与mock的唯一区别就是默认行为不一样:spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值。
Demo:
//假设目标类的实现是这样的
public class PasswordValidator {
public boolean verifyPassword(String password) {
return "xiaochuang_is_handsome".equals(password);
}
}
@Test
public void testSpy() {
//跟创建mock类似,只不过调用的是spy方法,而不是mock方法。spy的用法
PasswordValidator spyValidator = Mockito.spy(PasswordValidator.class);
//在默认情况下,spy对象会调用这个类的真实逻辑,并返回相应的返回值,这可以对照上面的真实逻辑
spyValidator.verifyPassword("xiaochuang_is_handsome"); //true
spyValidator.verifyPassword("xiaochuang_is_not_handsome"); //false
//spy对象的方法也可以指定特定的行为
Mockito.when(spyValidator.verifyPassword(anyString())).thenReturn(true);
//同样的,可以验证spy对象的方法调用情况
spyValidator.verifyPassword("xiaochuang_is_handsome");
Mockito.verify(spyValidator).verifyPassword("xiaochuang_is_handsome"); //pass
}
4.PowerMock介绍
PowerMock是用来扩展Mockito功能,弥补其局限性(Mockito不能mock private、static方法和类,一些版本不能mock final方法和类),同时PowerMock还增加了很多反射方法来修改静态和非静态成员等。
PowerMock是依赖Mockito的,所以使用时要同时引入,且版本也必须一一对应。
PowerMock跟Mockito的版本对应关系如下:
Mockito | PowerMock |
---|---|
2.8.9+ | 2.x |
2.8.0-2.8.9 | 1.7.x |
2.7.5 | 1.7.0RC4 |
2.4.0 | 1.7.0RC2 |
2.0.0-beta - 2.0.42-beta | 1.6.5-1.7.0RC |
1.10.8 - 1.10.x | 1.6.2 - 2.0 |
1.9.5-rc1 - 1.9.5 | 1.5.0 - 1.5.6 |
1.9.0-rc1 & 1.9.0 | 1.4.10 - 1.4.12 |
1.8.5 | 1.3.9 - 1.4.9 |
1.8.4 | 1.3.7 & 1.3.8 |
1.8.3 | 1.3.6 |
1.8.1 & 1.8.2 | 1.3.5 |
1.8 | 1.3 |
1.7 | 1.2.5 |
下面举个public方法中调用private方法的例子,验证private方法是否被调用
public class PowerMockTest {
public int division(int x,int y){
if (y == 0){
return getResultByZeroDivisor();
}else {
return x/y;
}
}
private int getResultByZeroDivisor(){
return 0;
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({PowerMockTest.class})
public class PowerMockTestTest {
@Test
public void division() {
//mock
PowerMockTest test = Mockito.spy(PowerMockTest.class);
int division = test.division(1, 0);
assertEquals(0,division);
}
@Test
public void testPrivateMethod() throws Exception {
//powermock
PowerMockTest test = PowerMockito.spy(new PowerMockTest());
test.division(1,0);
PowerMockito.verifyPrivate(test,times(1)).invoke("getResultByZeroDivisor",new Object[]{});
}
}
测试结果如下图:
![](https://img.haomeiwen.com/i16714432/d10e3b5553fb6a28.png)
这里改变下入参 y =1
@Test
public void testPrivateMethod() throws Exception {
PowerMockTest test = PowerMockito.spy(new PowerMockTest());
test.division(1,1);
PowerMockito.verifyPrivate(test,times(1)).invoke("getResultByZeroDivisor",new Object[]{});
}
结果报错了,因为private方法未被调用
![](https://img.haomeiwen.com/i16714432/4308620de395c260.png)
-
@RunWith(PowerMockRunner.class) :这里使用的Runner是PowerMockRunner,这样就可以与Mokito兼容了,即Mokito里的方法在这里也都是可以正常使用的;
-
@PrepareForTest({Calculator.class}):务必记得加上这个PrepareForTest的注解,否则进行verify测试的时候怎么测试都是pass的,如下没有写这个注解,verifyPrivate()怎么测试都是pass。
常用测试方法
1.创建模拟对象的2种姿势
- mock
activity = PowerMockito.mock(new MainActivity())
//使activity的isFinishing方法总是返回true
when(activity.isFinishing()).thenReturn(true);
通过mock创造出来的对象,调用该对象所有方法都不会执行真实逻辑。必须结合when(...).then(...)来使模拟对象按照我们预期返回。
- spy
activity = PowerMockito.spy(new MainActivity())
//使activity的isFinishing方法总是返回false
PowerMockito.doReturn(false).when(activity).isFinishing();
通过spy创造模拟对象必须先手动new出来,调用该对象所有方法都会执行真实逻辑。
spy对象必须结合doReturn(...).when(...)才会忽略真实逻辑,并按照我们预期返回。如果函数返回值为void,可以用doNothing()代替doReturn()。
2.访问/调用private
有时候被测类绝大部分是private函数(比如Activity),传统的单元测试很难覆盖到这些private函数,当然我们可以通过重构/封装使我们的业务代码对测试更友好,但为了测试而对原本稳定的业务代码进行侵入式的修改,在短期内肯定会带来不稳定因素,这往往是团队/领导无法容忍的。
PowerMock的Whitebox类提供了一组api可以获取/修改private的变量和函数,可以帮助我们绕过重构去对业务代码进行测试。
//修改私有变量
Whitebox.setInternalState(..)
//访问私有变量
Whitebox.getInternalState(..)
//调用私有函数
Whitebox.invokeMethod(..)
//调用私有的构造函数
Whitebox.invokeConstructor(..)
mock静态方法
public static boolean isEmpty(String s){
return s == null || s.length() == 0;
}
@Test
public void isEmpty(){
PowerMockito.mockStatic(PowerMockTest.class);
PowerMockito.when(PowerMockTest.isEmpty("xxx")).thenReturn(false);
assertFalse(PowerMockTest.isEmpty("xxx"));
}
3.抑制不必要的代码逻辑执行
在实际项目中会有很多常用但不影响业务逻辑的代码(Log以及其他统计代码等等),有些静态代码块也直接调用Android SDK api。因为单元测试代码运行在JVM上,这些代码很容易会报错,如果为了测试去修改这些代码未免有点本末倒置,所以我们在单元测试的过程中需要抑制/隔离这些代码的执行
抑制静态变量/代码块的执行
PowerMockito提供了@SuppressStaticInitializationFor注解:
//在单元测试类之前声明以下注解,可以阻止FileUtil类的静态代码块运行
@SuppressStaticInitializationFor("com.colin.unittest.FileUtil")
public class PowerMockitoSampleIII {
...
}
抑制Log等静态函数的执行
借助mockStatic可以使指定类的静态方法不执行。
@PrepareForTest(Log.class)
public class PowerMockitoSampleIII {
@Before
public void setUp() throws Exception {
//抑制Log相关代码的执行
PowerMockito.mockStatic(Log.class);
}
...
}
抑制super函数()的执行
实际业务开发中,我们经常需要继承Android SDK的类来进行扩展,对这些类覆写的函数进行单元测试时,往往需要抑制父类super()的逻辑,不然在JVM中执行单元测试代码时会报错。
//抑制MainActivity父类的onDestroy方法
Method method = PowerMockito.method(MainActivity.class.getSuperclass(),
"onDestroy");
PowerMockito.suppress(method);
网友评论