Android自动化测试入门(四)单元测试

Android自动化测试入门(四)单元测试

单元测试一般分两类:

  • 本地测试:运行在本地的计算机上,这些测试编译之后可以直接运行在本地的Java虚拟机上(JVM)。可以最大限度的缩短执行的时间。如果测试中用到了Android框架中的对象,那么谷歌推荐使用Robolectric来模拟对象。
  • 插桩测试:在Android设备或者模拟器上运行的测试,这些测试可以访问插桩测试信息,比如被测设备的Context,使用此方法可以运行具有复杂Android依赖的单元测试。前两篇中的Espresso 和 UI Automator就是这类测试,Espresso一般用来测试单个界面,UI Automator一般用来测试多界面交互。它们运行的比本地测试慢很多,所以谷歌建议最好是必须针对设备测试的时候才使用。

本地单元测试在Android自动化测试中是比重最大的一环,主要针对某个类中的某个方法。谷歌建议在所有的测试中,单元测试要占到70%的比重,为啥它就这么重要呢?

  • 本地单元测试相比于前面几篇中的UI测试执行效率高,前面的UI测试是需要运行在手机上的,所以想要运行测试就需要执行代码的编译、打包、安装、运行,这是非常耗时的,特别是工程很大的时候,运行一次可能需要很长的时间。如果我们只是改变了代码中的一个方法,使用单元测试可以快速验证该方法的正确性。
  • 提高写代码的抽象和封装能力,比如刚入行的时候,我们可能在一个按钮的OnClickListener方法中写一大坨代码,如果了解单元测试就会知道这样写对测试非常不友好,把这一坨提取封装会更利于测试,也就能更快的验证代码的正确性。
  • 因为单元测试是独立的单个方法的测试,那么当测试结果与预期不一致的时候,可以迅速定位bug。
  • 提高代码的稳定性,和易维护性,写代码的时候能确保正确开发,在修改代码之后,保证功能不被破坏,其实编写单元测试的过程也是对代自己写的代码的Code Review,是对代码持续重构的开始。

本部分会用到四个小东西,Junit,Mockito,PowerMockito,Robolectric。Junit是单元测试框架,Mockito和Robolectric都是用来产生模拟对象的,Mockito在Java中用的多,PowerMockito是Mockito的增强版可以模拟final,static,private等Mockito不能mock的方法,Robolectric可以模拟更多的Andorid框架中的对象。

  • 如果要构建的本地单元测试对Android框架依赖小,可以选择mockito,速度更快。
  • 如果要构建的本地单元测试对Android框架有很大的依赖性,可以选择Robolectric

Junit

Junit是java中非常有名的测试框架,让测试变得很容易。假如下面我们有一个toNumber的方法要测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

}

为了保证测试的全面性,我们可能需要设计下面的几个测试用例

  • 如果传入的是null,那么应该返回null
  • 如果传入的全是数字比如”12321”,那么应该返回整数12321
  • 如果传入的字符串左边或者右边,或者两边都有空格比如”123 “,” 123”,” 123 “,那么应该返回正确的整数123
  • 如果传入的字符串中间有空格,或者有字母比如””12 3”,”12ab”,这时候会发生崩溃,我们不让他崩溃,让他返回null

测试代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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()就完事了,随着单元测试写完并且测试用例都通过之后,这个方法也会变的更加健壮,变成了前面代码中所写的那样。

mockito

Junit已经能完成单元测试了,为啥要使用Mockito或者Robolectric?

我们需要明确单元测试的目的:单元测试的目的是为了测试我们自己写的代码的正确性,它不需要测试外部的各种依赖,所以当我们遇到一个方法中有很多别的对象的依赖的时候,比如操作数据库,连接网络,读写文件等等,需要给它解依赖。

怎么解依赖呢?其实就是弄一些假对象,比如代码中是我们从网络获取一段json数据,转化成一个对象传入到我们的测试方法中。那么就可以直接new一个假的对象,并给它设置我们期望的返回值传给要测试的方法就好了,不需要再去请求网络获取数据。这个过程称之为mock

直接手动去new一个对象,然后去设置各种数据是比较麻烦的,而Mockito这类的框架就是用来简化我们手动mock的。使用他们来创建一个虚拟对象设置返回值等操作会变得非常简单。

下面开始练习,测试代码写在 src/main/test/java文件夹下面

先练习使用mockito,引入依赖库

1
testImplementation 'org.mockito:mockito-core:3.0.0'

新建一个MockitoTest类,在类上添加注解@RunWith(MockitoJUnitRunner.class)表示Junit要把测试方法运行在MockitoJUnitRunner上

1
2
@RunWith(MockitoJUnitRunner.class)
public class MockitoTest {......}

例子1: 结果验证,测试某些结果是否正确,使用when和thenReturn表示当调用某个方法的时候指定返回值。最后通过assertEquals判断返回值是否正确

1
2
3
4
5
6
7
8
9
10
11
@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方法可以验证某个方法是否执行过,执行的次数

1
2
3
4
5
6
7
8
9
10
11
12
13
@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

1
2
3
4
5
6
7
@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一个对象比如

1
2
3
4
5
6
7
@Mock
List<Integer> mList;
@Test
public void testAnnotationMock(){
mList.add(0);
verify(mList).add(0);
}

例子5: 可以验证是否执行了某个参数的方法

1
2
3
4
5
6
7
8
9
10
11
@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对象的方法默认什么都不做,或直接返回默认值。

1
2
3
4
5
6
7
8
9
10
11
@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

powermock官网

首先添加依赖

1
2
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方法

目标方法

1
2
3
public static boolean isEmpty(@Nullable CharSequence str) {
return str == null || str.length() == 0;
}

测试方法

1
2
3
4
5
6
@Test
public void testStatic(){
PowerMockito.mockStatic(Utils.class);
PowerMockito.when(Utils.isEmpty("abc")).thenReturn(false);
assertFalse(Utils.isEmpty("abc"));
}

例子2: 测试private方法 替换私有变量

目标方法

1
2
3
4
5
6
7
8
private String name;

private String changeName(String name) {
return "ABC" + name;
}
public String getName() {
return name;
}

测试方法

1
2
3
4
5
6
7
8
9
10
@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关键字

目标方法

1
2
3
4
public String getPersonName() {
Person person = new Person("Lily");
return person.getName();
}

测试方法

1
2
3
4
5
6
7
8
9
@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方法就不会报错了。

Robolectric

前面测试的类和依赖都是原生Java代码,可以直接运行在JVM上,当我们测试Android的时候,需要依赖Android SDK中的android.jar包,android.jar底层没有具体的代码实现,因为它运行在Andorid系统中,Android系统中有默认的实现。

Mockito和PowerMockito都直接运行在JVM上,JVM上没有Android源码相关的实现,那么在做有Adroid相关的依赖的测试的时候,就会报错,这时候就要用到Robolectric啦,当我们去调用android相关的代码的时候,它会拦截并去执行自己对相关代码的实现。

Robolectric官网

添加依赖

1
2
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

1
2
3
4
android {
compileSdkVersion 28
testOptions.unitTests.includeAndroidResources = true
}

在gradle.properties文件中添加下面代码

1
android.enableUnitTestBinaryResources=true

第一次运行的时候会下载相关jar包,网速不好可能要等很久

首先创建一个测试类RobolectricTest,添加注解@RunWith(RobolectricTestRunner.class)通知Junit框架该类中的测试方法运行在RobolectricTestRunner中。

1
2
@RunWith(RobolectricTestRunner.class)
public class RobolectricTest {...}

例子1: 点击button,改变TextView上的文字,判断改变之后的文字是不是预期的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    @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是不是触发了真正的意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  //Application用的比较多,可以初始换一个全局的
private Application context;

@Before
public void setUp() throws Exception {
context = ApplicationProvider.getApplicationContext();
}
@Test
public void testClickButtonToPicking() {
ActivityScenario<MainActivity> 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显示

1
2
3
4
5
6
7
8
9
10
11
12
@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单元测试

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×