内容简介:Android自动化测试入门(四)单元测试单元测试一般分两类:本地单元测试在Android自动化测试中是比重最大的一环,主要针对某个类中的某个方法。谷歌建议在所有的测试中,单元测试要占到70%的比重,为啥它就这么重要呢?
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的方法要测试
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
测试代码如下
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,引入依赖库
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方法就不会报错了。
Robolectric
前面测试的类和依赖都是原生Java代码,可以直接运行在JVM上,当我们测试Android的时候,需要依赖Android SDK中的android.jar包,android.jar底层没有具体的代码实现,因为它运行在Andorid系统中,Android系统中有默认的实现。
Mockito和PowerMockito都直接运行在JVM上,JVM上没有Android源码相关的实现,那么在做有Adroid相关的依赖的测试的时候,就会报错,这时候就要用到Robolectric啦,当我们去调用android相关的代码的时候,它会拦截并去执行自己对相关代码的实现。
添加依赖
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<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显示
@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
本篇对本地单元测试的一些常用的库做了一些练习,练习完成就算是入门了,之后写单元测试哪里不熟悉就直接去查文档了。 而通过本篇练习本篇最主要的收获就是,以后写代码的时候要时刻有测试意识,尽最大努力写出可测试易维护的代码 。
参考:
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Vue 应用单元测试的策略与实践 02 - 单元测试基础
- Vue 应用单元测试的策略与实践 04 - Vuex 单元测试
- Vue 应用单元测试的策略与实践 03 - Vue 组件单元测试
- Angular单元测试系列-Component、Directive、Pipe 以及Service单元测试
- 单元测试,只是测试吗?
- 单元测试和集成测试业务
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Bulletproof Web Design
Dan Cederholm / New Riders Press / 28 July, 2005 / $39.99
No matter how visually appealing or packed with content a Web site is, it isn't succeeding if it's not reaching the widest possible audience. Designers who get this guide can be assured their Web site......一起来看看 《Bulletproof Web Design》 这本书的介绍吧!