单元测试一般分两类:
本地单元测试在Android自动化测试中是比重最大的一环,主要针对某个类中的某个方法。谷歌建议在所有的测试中,单元测试要占到70%的比重,为啥它就这么重要呢?
本部分会用到四个小东西,Junit,Mockito,PowerMockito,Robolectric。Junit是单元测试框架,Mockito和Robolectric都是用来产生模拟对象的,Mockito在Java中用的多,PowerMockito是Mockito的增强版可以模拟final,static,private等Mockito不能mock的方法,Robolectric可以模拟更多的Andorid框架中的对象。
Junit是java中非常有名的测试框架,让测试变得很容易。假如下面我们有一个toNumber的方法要测试
public class Utils {
public Integer toNumber(String num){
if(num == null || num.isEmpty()){
return null;
}
Integer integer;
try {
integer = Integer.parseInt(num.trim());
}catch (Exception e){
integer = null;
}
return integer;
}
}
为了保证测试的全面性,我们可能需要设计下面的几个测试用例
测试代码如下
public class ExampleUnitTest {
@Test
public void testToNumber_NotNullOrEmpty(){
Utils utils = new Utils();
assertNull(utils.toNumber(null));
assertNull(utils.toNumber(""));
}
@Test
public void testToNumber_hasSpace(){
Utils utils = new Utils();
assertEquals(new Integer("123"),utils.toNumber("123"));
assertEquals(new Integer("123"),utils.toNumber("123 "));
assertEquals(new Integer("123"),utils.toNumber(" 123 "));
}
@Test
public void testToNumber_hasMiddleSpace(){
Utils utils = new Utils();
assertNull(utils.toNumber("12 3"));
assertNull(utils.toNumber("12a3"));
}
}
其实写单元测试也是对自己代码的一次检查和重构,比如上面的toNumber方法,第一次写的时候可能有很多问题都没有想到直接返回一个Integer.parseInt()
就完事了,随着单元测试写完并且测试用例都通过之后,这个方法也会变的更加健壮,变成了前面代码中所写的那样。
Junit已经能完成单元测试了,为啥要使用Mockito或者Robolectric?
我们需要明确单元测试的目的:单元测试的目的是为了测试我们自己写的代码的正确性,它不需要测试外部的各种依赖,所以当我们遇到一个方法中有很多别的对象的依赖的时候,比如操作数据库,连接网络,读写文件等等,需要给它解依赖。
怎么解依赖呢?其实就是弄一些假对象,比如代码中是我们从网络获取一段json数据,转化成一个对象传入到我们的测试方法中。那么就可以直接new一个假的对象,并给它设置我们期望的返回值传给要测试的方法就好了,不需要再去请求网络获取数据。这个过程称之为mock
直接手动去new一个对象,然后去设置各种数据是比较麻烦的,而Mockito这类的框架就是用来简化我们手动mock的。使用他们来创建一个虚拟对象设置返回值等操作会变得非常简单。
下面开始练习,测试代码写在 src/main/test/java文件夹下面
先练习使用mockito,引入依赖库
testImplementation 'org.mockito:mockito-core:3.0.0'
新建一个MockitoTest类,在类上添加注解@RunWith(MockitoJUnitRunner.class)表示Junit要把测试方法运行在MockitoJUnitRunner上
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {......}
例子1: 结果验证,测试某些结果是否正确,使用when和thenReturn表示当调用某个方法的时候指定返回值。最后通过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: 通过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
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出来的对象都是虚拟的对象,我们可以验证其执行次数,状态等,如果一个对象是真实的,那怎么验证呢 可以使用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();
}
Mockito虽然好用但是也有些不足,比如不能mock static、final、private等对象,使用PowerMock就可以实现了
powermock官网
首先添加依赖
testImplementation 'org.powermock:powermock-module-junit4:2.0.2'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
创建一个PowerMockTest类,在类上添加注解@RunWith(PowerMockRunner.class)
,通知Junit该类的测试方法运行在PowerMockRunner中。在添加注解@PrepareForTest(Utils.class)
表示要测试的方法所在的类,这里是一个自定义的Utils.class
例子1: 测试static方法
目标方法
public static boolean isEmpty(@Nullable CharSequence str) {
return str == null || str.length() == 0;
}
测试方法
@Test
public void testStatic(){
PowerMockito.mockStatic(Utils.class);
PowerMockito.when(Utils.isEmpty("abc")).thenReturn(false);
assertFalse(Utils.isEmpty("abc"));
}
例子2: 测试private方法 替换私有变量
目标方法
private String name;
private String changeName(String name) {
return "ABC" + name;
}
public String getName() {
return name;
}
测试方法
@Test
public void testPrivate() throws Exception {
Utils util = new Utils();
//调用私有方法
String res = Whitebox.invokeMethod(util, "changeName", "Lily");
assertEquals("ABCLily",res);
//替换私有变量 也可以使用MemberModifier来修改
Whitebox.setInternalState(util,"name","Lily");
assertEquals("Lily",util.getName());
}
例子3: 测试mock new关键字
目标方法
public String getPersonName() {
Person person = new Person("Lily");
return person.getName();
}
测试方法
@Test
public void testNew() throws Exception {
Person person = PowerMockito.mock(Person.class);
Utils util = new Utils();
//当new一个Person对象并传入Lily的时候,返回person
PowerMockito.whenNew(Person.class).withArguments("Lily").thenReturn(person);
PowerMockito.when(util.getPersonName()).thenReturn("Diavd");
assertEquals("Diavd",util.getPersonName());
}
目标方法getPersonName中new了一个Person,直接调用getPersonName方法会报错,所以我们自己创建一个Person,并指定当当new一个Person对象并传入Lily的时候,返回当前创建的person对象。然后在调用getPersonName方法就不会报错了。
前面测试的类和依赖都是原生Java代码,可以直接运行在JVM上,当我们测试Android的时候,需要依赖Android SDK中的android.jar包,android.jar底层没有具体的代码实现,因为它运行在Andorid系统中,Android系统中有默认的实现。
Mockito和PowerMockito都直接运行在JVM上,JVM上没有Android源码相关的实现,那么在做有Adroid相关的依赖的测试的时候,就会报错,这时候就要用到Robolectric啦,当我们去调用android相关的代码的时候,它会拦截并去执行自己对相关代码的实现。
Robolectric官网
添加依赖
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.robolectric:robolectric:4.3.1'
Robolectric 4.0以上需要Android Gradle插件/ Android Studio 3.2或更高版本。
在build.gradle中的android闭包下面添加下面代码,目前版本最高支持andorid sdk 28
android {
compileSdkVersion 28
testOptions.unitTests.includeAndroidResources = true
}
在gradle.properties文件中添加下面代码
android.enableUnitTestBinaryResources=true
第一次运行的时候会下载相关jar包,网速不好可能要等很久
首先创建一个测试类RobolectricTest,添加注解@RunWith(RobolectricTestRunner.class)
通知Junit框架该类中的测试方法运行在RobolectricTestRunner中。
@RunWith(RobolectricTestRunner.class)
public class RobolectricTest {...}
例子1: 点击button,改变TextView上的文字,判断改变之后的文字是不是预期的
@Test
public void clickingButtonShouldChangeMessage() {
//默认会调用Activity的onCreate()、onStart()、onResume()
// MainActivity activity = Robolectric.setupActivity(MainActivity.class);
// TextView textView = activity.findViewById(R.id.tv_text);
// Button button = activity.findViewById(R.id.btn_click);
// button.performClick();
// assertThat(textView.getText().toString(), equalTo("Hello Espresso!"));
//Robolectric.setupActivity显示过时了,使用ActivityScenario来代替
//ActivityScenario提供api来启动和驱动Activity的生命周期状态以进行测试,
// 适用于任意Activity,并能在不同版本的Android上一致工作
//通过scenario.moveToState来控制生命周期比如 scenario.moveToState(Lifecycle.State.CREATED)
ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
TextView textView = activity.findViewById(R.id.tv_text);
Button button = activity.findViewById(R.id.btn_click);
button.performClick();
assertThat(textView.getText().toString(), equalTo("Hello Espresso!"));
});
}
使用Robolectric.setupActivity可以启动一个Activity,不过使用的时候显示该方法已过期,最新的可以使用ActivityScenario来启动一个Activity
ActivityScenario提供api来启动和驱动Activity的生命周期状态以进行测试,适用于任意Activity,并能在不同版本的Android上一致工作,通过scenario.moveToState来控制生命周期比如 scenario.moveToState(Lifecycle.State.CREATED)
例子2: 点击按钮从MainActivity到UnitTestActivity,Robolectric是运行在JVM上的测试框架,并不会真正的启动UnitTestActivity,但是可以检查MainActivity是不是触发了真正的意图
//Application用的比较多,可以初始换一个全局的
private Application context;
@Before
public void setUp() throws Exception {
context = ApplicationProvider.getApplicationContext();
}
@Test
public void testClickButtonToPicking() {
ActivityScenario scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
Button button = activity.findViewById(R.id.btn_go_to_unit);
button.performClick();
//期望的intent
Intent expectedIntent = new Intent(activity, UnitTestActivity.class);
//真实的intent
Intent actual = shadowOf(context)
.getNextStartedActivity();
assertEquals(expectedIntent.getComponent(),actual.getComponent());
});
}
例子3: Shadow是Robolectric的核心,Robolectric中内置了很多Android SDK中的类的影子,比如ShadowCompoundButton,ShadowTextView,ShadowActivity …
当一个android.jar中的某个类被调用的时候,Robolectric会尝试寻找该类的影子,调用影子中的方法,通过shadowOf可以很方便的拿到对应类的影子类
测试Toast显示
@Test
public void testToast(){
ActivityScenario<MainActivity> scenario = ActivityScenario.launch(MainActivity.class);
scenario.onActivity(activity -> {
Button button = activity.findViewById(R.id.btn_show_toast);
button.performClick();
Toast latestToast = ShadowToast.getLatestToast();
assertNotNull(latestToast);
assertEquals("测试Toast", ShadowToast.getTextOfLatestToast());
});
}
更多例子可查看源码 Robolectric
本篇对本地单元测试的一些常用的库做了一些练习,练习完成就算是入门了,之后写单元测试哪里不熟悉就直接去查文档了。而通过本篇练习本篇最主要的收获就是,以后写代码的时候要时刻有测试意识,尽最大努力写出可测试易维护的代码。
参考:
Android 官网测试文档
Android单元测试与模拟测试
使用强大的 Mockito 来测试你的代码
Android单元测试(一)
Android单元测试(二)
Mockito与PowerMock的使用基础教程
Mockito教程
一文全面了解Android单元测试