测试驱动式编程(Test-Driven-Development)在RoR中已经是非常普遍的开发模式,是一种十分可靠、优秀的编程思想,可是在Android领域中这块还没有普及,今天主要聊聊Android中的单元测试与模拟测试及其常用的一些库。
关于Kotlin中的单元测试,请移步这篇文章: Kotlin
I. 测试与基本规范 1. 为什么需要测试? 为了稳定性,能够明确的了解是否正确的完成开发。 更加易于维护,能够在修改代码后保证功能不被破坏。 集成一些工具,规范开发规范,使得代码更加稳定( 如通过 phabricator differential 发diff时提交需要执行的单元测试,在开发流程上就可以保证远端代码的稳定性)。 2. 测什么? 一般单元测试: 模拟测试: 根据需求,测试用户真正在使用过程中,界面的反馈与显示以及一些依赖系统架构的组件的应用测试。 3. 需要注意 考虑可读性,对于方法名使用表达能力强的方法名,对于测试范式可以考虑使用一种规范, 如 RSpec-style。方法名可以采用一种格式,如: [测试的方法]_[测试的条件]_[符合预期的结果]
。 不要使用逻辑流关键字(If/else、for、do/while、switch/case),在一个测试方法中,如果需要有这些,拆分到单独的每个测试方法里。 测试真正需要测试的内容,需要覆盖的情况,一般情况只考虑验证输出(如某操作后,显示什么,值是什么)。 考虑耗时,Android Studio默认会输出耗时。 不需要考虑测试private
的方法,将private
方法当做黑盒内部组件,测试对其引用的public
方法即可;不考虑测试琐碎的代码,如getter
或者setter
。 每个单元测试方法,应没有先后顺序;尽可能的解耦对于不同的测试方法,不应该存在Test A与Test B存在时序性的情况。 4. 创建测试 选择对应的类 将光标停留在类名上 按下ALT + ENTER
在弹出的弹窗中选择Create Test
II. Android Studio中的单元测试与模拟测试 control + shift + R (Android Studio 默认执行单元测试快捷键)。
1. 本地单元测试 直接在开发机上面进行运行测试。 在没有依赖或者仅仅只需要简单的Android库依赖的情况下,有限考虑使用该类单元测试。
./gradlew test
通过添加以下脚本到对应module的build.gradle
中,以便于在终端中也可以直接查看单元测试的各类测试信息:
1 2 3 4 5 6 7 8 9 10 android { ... testOptions.unitTests.all { testLogging { events 'passed' , 'skipped' , 'failed' , 'standardOut' , 'standardError' outputs.upToDateWhen { false } showStandardStreams = true } } }
代码存储 如果是对应不同的flavor或者是build type,直接在test后面加上对应后缀(如对应名为myFlavor
的单元测试代码,应该放在src/testMyFlavor/java
下面)。
src/test/java
Google官方推荐引用 1 2 3 4 5 6 dependencies { testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' }
JUnit Annotation Annotation 描述 @Test public void method()
定义所在方法为单元测试方法
@Test (expected = Exception.class)
如果所在方法没有抛出Annotation
中的Exception.class
->失败 @Test(timeout=100)
如果方法耗时超过100
毫秒->失败 @Test(expected=Exception.class)
如果方法抛了Exception.class类型的异常->通过 @Before public void method()
这个方法在每个测试之前执行,用于准备测试环境(如: 初始化类,读输入流等) @After public void method()
这个方法在每个测试之后执行,用于清理测试环境数据 BeforeClass public static void method()
这个方法在所有测试开始之前执行一次,用于做一些耗时的初始化工作(如: 连接数据库) AfterClass public static void method()
这个方法在所有测试结束之后执行一次,用于清理数据(如: 断开数据连接) @Ignore
或者@Ignore("Why disabled")
忽略当前测试方法,一般用于测试方法还没有准备好,或者太耗时之类的 @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class TestClass{}
使得该测试方法中的所有测试都按照方法中的字母顺序测试 Assume.assumeFalse(boolean condition)
如果满足condition
,就不执行对应方法
Exception 处理 比如我们希望执行某方法之后,该方法需要抛出IOException
,最简单的方法如下:
1 2 3 4 5 6 7 8 9 @Test public void thrownExceptionTestMethod () { try { doSomethingThatShouldThrow(); Assert.fail("Should have thrown IO exception" ); } catch (IOException e) { } }
如果仅仅只需要检验是否有抛出,我们可以通过expected
来简化:
1 2 3 4 @Test(expected = IOException.class) public void thrownExceptionTestMethod () { doSomethingThatShouldThrow(); }
如果需要校验Exception
的内容,也可以通过ExpectedException
来简化:
1 2 3 4 5 6 7 8 9 10 11 @Rule public ExpectedException thrown = ExpectedException.none();@Test public void shouldTestExceptionMessage () throws IndexOutOfBoundsException { List<Object> list = new ArrayList <Object>(); thrown.expect(IndexOutOfBoundsException.class); thrown.expectMessage("Index: 0, Size: 0" ); list.get(0 ); }
2. 模拟测试 需要运行在Android设备或者虚拟机上的测试。
主要用于测试: 单元(Android SDK层引用关系的相关的单元测试)、UI、应用组件集成测试(Service、Content Provider等)。
./gradlew connectedAndroidTest
代码存储: src/androidTest/java
Google官方推荐引用 1 2 3 4 5 6 7 8 9 10 11 dependencies { androidTestCompile 'com.android.support:support-annotations:23.0.1' androidTestCompile 'com.android.support.test:runner:0.4.1' androidTestCompile 'com.android.support.test:rules:0.4.1' androidTestCompile 'org.hamcrest:hamcrest-library:1.3' androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1' }
常见的UI测试 需要模拟Android系统环境。
主要三点: UI加载好后展示的信息是否正确。 在用户某个操作后UI信息是否展示正确。 展示正确的页面供用户操作。 Espresso 谷歌官方提供用于UI交互测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import static android.support.test.espresso.Espresso.onView;import static android.support.test.espresso.action.ViewActions.click;import static android.support.test.espresso.assertion.ViewAssertions.matches;import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;import static android.support.test.espresso.matcher.ViewMatchers.withId; onView(withId(R.id.my_view)).perform(click()) .check(matches(isDisplayed())); onView(withText(startsWith("ABC" ))).check(matches(not(isEnabled())); pressBack(); onView(withId(R.id.button)).check(matches(withText(("Start new activity" )))); onView(withId(R.id.viewId)).check(matches(withText(not(containsString("YYZZ" ))))); onView(withId(R.id.inputField)).perform(typeText("NewText" ), closeSoftKeyboard()); onView(withId(R.id.inputField)).perform(clearText());
启动一个打开Activity
的Intent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RunWith(AndroidJUnit4.class) public class SecondActivityTest { @Rule public ActivityTestRule<SecondActivity> rule = new ActivityTestRule (SecondActivity.class, true , false ); @Test public void demonstrateIntentPrep () { Intent intent = new Intent (); intent.putExtra("EXTRA" , "Test" ); rule.launchActivity(intent); onView(withId(R.id.display)).check(matches(withText("Test" ))); } }
异步交互 建议关闭设备中”设置->开发者选项中”的动画,因为这些动画可能会是的Espresso在检测异步任务的时候产生混淆: 窗口动画缩放(Window animation scale)、过渡动画缩放(Transition animation scale)、动画程序时长缩放(Animator duration scale)。
针对AsyncTask
,在测试的时候,如触发点击事件以后抛了一个AsyncTask
任务,在测试的时候直接onView(withId(R.id.update)).perform(click())
,然后直接进行检测,此时的检测就是在AsyncTask#onPostExecute
之后。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public class IntentServiceIdlingResource implements IdlingResource { ResourceCallback resourceCallback; private Context context; public IntentServiceIdlingResource (Context context) { this .context = context; } @Override public String getName () { return IntentServiceIdlingResource.class.getName(); } @Override public void registerIdleTransitionCallback ( ResourceCallback resourceCallback) { this .resourceCallback = resourceCallback; } @Override public boolean isIdleNow () { boolean idle = !isIntentServiceRunning(); if (idle && resourceCallback != null ) { resourceCallback.onTransitionToIdle(); } return idle; } private boolean isIntentServiceRunning () { ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); List<ActivityManager.RunningServiceInfo> runningServices = manager.getRunningServices(Integer.MAX_VALUE); for (ActivityManager.RunningServiceInfo info : runningServices) { if (MyIntentService.class.getName().equals(info.service.getClassName())) { return true ; } } return false ; } }@RunWith(AndroidJUnit4.class) public class IntegrationTest { @Rule public ActivityTestRule rule = new ActivityTestRule (MainActivity.class); IntentServiceIdlingResource idlingResource; @Before public void before () { Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); Context ctx = instrumentation.getTargetContext(); idlingResource = new IntentServiceIdlingResource (ctx); Espresso.registerIdlingResources(idlingResource); } @After public void after () { Espresso.unregisterIdlingResources(idlingResource); } @Test public void runSequence () { onView(withId(R.id.action_settings)).perform(click()); onView(withText("Broadcast" )).check(matches(notNullValue())); } }
自定义匹配器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public static Matcher<View> withItemHint (String itemHintText) { checkArgument(!(itemHintText.equals(null ))); return withItemHint(is(itemHintText)); }public static Matcher<View> withItemHint (final Matcher<String> matcherText) { checkNotNull(matcherText); return new BoundedMatcher <View, EditText>(EditText.class) { @Override public void describeTo (Description description) { description.appendText("with item hint: " + matcherText); } @Override protected boolean matchesSafely (EditText editTextField) { return matcherText.matches(editTextField.getHint().toString()); } }; } onView(withItemHint("test" )).check(matches(isDisplayed()));
III. 拓展工具 1. AssertJ Android square/assertj-android 极大的提高可读性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import static org.assertj.core.api.Assertions.*; assertThat(view).isGone();MyClass test = new MyClass ("Frodo" );MyClass test1 = new MyClass ("Sauron" );MyClass test2 = new MyClass ("Jacks" ); List<MyClass> testList = new ArrayList <>(); testList.add(test); testList.add(test1); assertThat(test.getName()).isEqualTo("Frodo" ); assertThat(test).isNotEqualTo(test1) .isIn(testList); assertThat(test.getName()).startsWith("Fro" ) .endsWith("do" ) .isEqualToIgnoringCase("frodo" ); assertThat(list).hasSize(2 ) .contains(test, test1) .doesNotContain(test2); assertThat(testList).extracting("name" ) .contains("Frodo" , "Sauron" ) .doesNotContain("Jacks" );
2. Hamcrest JavaHamcrest 通过已有的通配方法,快速的对代码条件进行测试org.hamcrest:hamcrest-junit:(version)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import static org.hamcrest.MatcherAssert.assertThat;import static org.hamcrest.Matchers.is;import static org.hamcrest.Matchers.equalTo; assertThat(a, equalTo(b)); assertThat(a, is(equalTo(b))); assertThat(a, is(b)); assertThat(actual, is(not(equalTo(b)))); List<Integer> list = Arrays.asList(5 , 2 , 4 ); assertThat(list, hasSize(3 )); assertThat(list, contains(5 , 2 , 4 )); assertThat(list, containsInAnyOrder(2 , 4 , 5 )); assertThat(list, everyItem(greaterThan(1 ))); assertThat(fellowship, everyItem(hasProperty("race" , is(not((ORC)))))); assertThat(object1, samePropertyValuesAs(object2)); Integer[] ints = new Integer [] { 7 , 5 , 12 , 16 }; assertThat(ints, arrayContaining(7 , 5 , 12 , 16 ));
几个主要的匹配器 Mather 描述 allOf
所有都匹配 anyOf
任意一个匹配 not
不是 equalTo
对象等于 is
是 hasToString
包含toString
instanceOf
,isCompatibleType
类的类型是否匹配 notNullValue
,nullValue
测试null sameInstance
相同实例 hasEntry
,hasKey
,hasValue
测试Map
中的Entry
、Key
、Value
hasItem
,hasItems
测试集合(collection
)中包含元素 hasItemInArray
测试数组中包含元素 closeTo
测试浮点数是否接近指定值 greaterThan
,greaterThanOrEqualTo
,lessThan
,lessThanOrEqualTo
数据对比 equalToIgnoringCase
忽略大小写字符串对比 equalToIgnoringWhiteSpace
忽略空格字符串对比 containsString
,endsWith
,startsWith
,isEmptyString
,isEmptyOrNullString
字符串匹配
自定义匹配器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import org.hamcrest.Description;import org.hamcrest.TypeSafeMatcher;public class RegexMatcher extends TypeSafeMatcher <String> { private final String regex; public RegexMatcher (final String regex) { this .regex = regex; } @Override public void describeTo (final Description description) { description.appendText("matches regular expression=`" + regex + "`" ); } @Override public boolean matchesSafely (final String string) { return string.matches(regex); } public static RegexMatcher matchesRegex (final String regex) { return new RegexMatcher (regex); } }String s = "aaabbbaaa" ; assertThat(s, RegexMatcher.matchesRegex("a*b*a" ));
3. Mockito Mockito Mock对象,控制其返回值,监控其方法的调用。org.mockito:mockito-all:(version)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import static org.mockito.Mockito.mock;import static org.mockito.Mockito.verify; MyClass test = mock(MyClass.class); when(test.getUniqueId()).thenReturn(43 ); when(test.compareTo(anyInt())).thenReturn(43 ); when(test.compareTo(isA(Target.class))).thenReturn(43 ); doThrow(new IOException ()).when(test).close(); doNothing().when(test).execute(); verify(test, times(2 )).getUniqueId(); verify(test, never()).getUniqueId(); verify(test, atLeast(2 )).getUniqueId(); verify(test, atMost(3 )).getUniqueId(); verify(test).query("test string" );List list = new LinkedList ();List spy = spy(list); doReturn("foo" ).when(spy).get(0 ); assertEquals("foo" , spy.get(0 ));
对访问方法时,传入参数进行快照 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 import org.mockito.ArgumentCaptor;import org.mockito.Captor;import static org.junit.Assert.assertEquals;@Captor private ArgumentCaptor<Integer> captor;@Test public void testCapture () { MyClass test = mock(MyClass.class); test.compareTo(3 , 4 ); verify(test).compareTo(captor.capture(), eq(4 )); assertEquals(3 , (int )captor.getValue()); ArgumentCaptor<String> varArgs = ArgumentCaptor.forClass(String.class); test.doSomething("param-1" , "param-2" ); verify(test).doSomething(varArgs.capture()); assertThat(varArgs.getAllValues()).contains("param-1" , "param-2" ); }
对于静态的方法的Mock 可以使用 PowerMock :
org.powermock:powermock-api-mockito:(version)
& org.powermock:powermock-module-junit4:(version)
(For PowerMockRunner.class
)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @RunWith(PowerMockRunner.class) @PrepareForTest({StaticClass1.class, StaticClass2.class}) public class MyTest { @Test public void testSomething () { mockStatic(StaticClass1.class); when(StaticClass1.getStaticMethod()).andReturn("anything" ); verifyStatic(time(1 )); StaticClass1.getStaticMethod(); when(StaticClass1.getStaticMethod()).andReturn("what ever" ); verifyStatic(atLeastOnce()); StaticClass2.getStaticMethod(); whenNew(File.class).withAnyArguments().thenReturn(fileInstance); } }
或者是封装为非静态,然后用Mockito:
1 2 3 4 5 class FooWraper { void someMethod () { Foo.someStaticMethod(); } }
4. Robolectric Robolectric 让模拟测试直接在开发机上完成,而不需要在Android系统上。所有需要使用到系统架构库的,如(Handler
、HandlerThread
)都需要使用Robolectric,或者进行模拟测试。
主要是解决模拟测试中耗时的缺陷,模拟测试需要安装以及跑在Android系统上,也就是需要在Android虚拟机或者设备上面,所以十分的耗时。基本上每次来来回回都需要几分钟时间。针对这类问题,业界其实已经有了一个现成的解决方案: Pivotal实验室推出的Robolectric 。通过使用Robolectrict模拟Android系统核心库的Shadow Classes
的方式,我们可以像写本地测试一样写这类测试,并且直接运行在工作环境的JVM上,十分方便。
5. Robotium RobotiumTech/robotium (Integration Tests)模拟用户操作,事件流测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class) public class MyActivityTest { @Test public void doSomethingTests () { Application application = RuntimeEnvironment.application; WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class); activity.findViewById(R.id.login).performClick(); Intent expectedIntent = new Intent (activity, LoginActivity.class); assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent); } }
通过模拟用户的操作的行为事件流进行测试,这类测试无法避免需要在虚拟机或者设备上面运行的。是一些用户操作流程与视觉显示强相关的很好的选择。
6. Test Butler linkedin/test-butler 避免设备/模拟器系统或者环境的错误,导致测试的失败。
通常我们在进行UI测试的时候,会遇到由于模拟器或者设备的错误,如系统的crash、ANR、或是未预期的Wifi、CPU罢工,或者是锁屏,这些外再环境因素导致测试不过。Test-Butler引入就是避免这些环境因素导致UI测试不过。
该库被谷歌官方推荐过 ,并且收到谷歌工程师的Review。
IV. 拓展思路 1. Android Robots Instrumentation Testing Robots - Jake Wharton
假如我们需要测试: 发送 $42 到 “foo@bar.com “,然后验证是否成功。
通常的做法
2. Robot思想 在写真正的UI测试的时候,只需要关注要测试什么,而不需要关注需要怎么测试,换句话说就是让测试逻辑与View或Presenter解耦,而与数据产生关系。
首先通过封装一个Robot去处理How的部分:
然后在写测试的时候,只关注需要测试什么:
最终的思想原理