Android上的測試種類
- Local Unit Test
在本機(jī)的Java VM上運(yùn)行
- 優(yōu)點(diǎn):運(yùn)行速度快,jenkins每次編譯時(shí)可以運(yùn)行,適合做TDD
- 缺點(diǎn):不能測試android代碼
2.Intrumented Test
運(yùn)行的時(shí)候生成測試apk和被測應(yīng)用的apk,安裝到手機(jī)上運(yùn)行
-優(yōu)點(diǎn):所有代碼都能測試
-缺點(diǎn):運(yùn)行速度慢,不能在jenkins編譯時(shí)運(yùn)行

Local Unit Test##
代碼位置
在src下創(chuàng)建test文件夾,如果測試代碼是針對某個(gè)flavor和build type的,則創(chuàng)建testFlavorBuildType的文件夾

在gradle中設(shè)置
testCompile 'junit:junit:4.12'
生成測試代碼
打開要測試類的代碼,選中類的名字,ctrl+shift+T

選擇要測試的方法,以及setUp和tearDown方法

生成的測試類

Junit 4注解
| 標(biāo)注 | 說明 |
|---|---|
| @Before | 標(biāo)注setup方法,每個(gè)單元測試用例方法調(diào)用之前都會(huì)調(diào)用 |
| @After | 標(biāo)注teardown方法,每個(gè)單元測試用例方法調(diào)用之后都會(huì)調(diào)用 |
| @Test | 標(biāo)注的每個(gè)方法都是一個(gè)測試用例 |
| @BeforeClass | 標(biāo)注的靜態(tài)方法,在當(dāng)前測試類所有用例方法執(zhí)行之前執(zhí)行 |
| @AfterClass | 標(biāo)注的靜態(tài)方法,在當(dāng)前測試類所有用例方法執(zhí)行之后執(zhí)行 |
| @Test(timeout=) | 為測試用例指定超時(shí)時(shí)間 |

斷言
Junit提供了一系列斷言來判斷是pass還是fail
| 方法 | 說明 |
|---|---|
| assertTrue(condition) | condition為真pass,否則fail |
| assertFalse(condition) | condition為假pass,否則fail |
| fail() | 直接fail |
| assertEquals(expected, actual) | expected equal actual pass,否則fail |
| assertSame(expected, actual) | expected == actual pass,否則fail |
更多方法查看Assert.java的靜態(tài)方法
運(yùn)行測試
IDE:點(diǎn)擊邊欄的箭頭可以運(yùn)行整個(gè)測試類或單個(gè)方法

Command line:gradle test,運(yùn)行整個(gè)unit test
測試代碼覆蓋率
android gradle 插件自帶jacoco進(jìn)行代碼測試覆蓋率的統(tǒng)計(jì)
根build.gralde配置
buildscript {
...
dependencies {
....
classpath 'com.dicedmelon.gradle:jacoco-android:0.1.1'
}
}
模塊build.gradle配置
apply plugin: 'jacoco-android'
buildTypes {
debug {
testCoverageEnabled = true
}
}
//需要排除在統(tǒng)計(jì)之外的類
jacocoAndroidUnitTestReport {
excludes += ['**/ApiConnectionImpl**']
}
cmd:
- gradle jacocoTestReport 生成全部flavor和buildtype的測試覆蓋率報(bào)告
- gradle jacocoTestFlavorBuildTypeUnitTestReport 指定flavor和buildtype的測試覆蓋率報(bào)告
生成的報(bào)告放在
module\build\reports\jacoco\jacocoTestFlavorBuildTypeUnitTestReport\html

Local unit test help libary
Hamcrest
testCompile 'org.hamcrest:hamcrest-library:1.3'
為斷言提供更好的可讀性(更接近與自然語言)
可讀性
Junit:
assertEquals(expected, actual);
Hamcrest:
assertThat(actual, is(equalTo(expected)));
Junit:
assertFalse(expected.equals(actual));
Hamcrest:
assertThat(actual, is(not(equalTo(expected))));
失敗信息更加詳細(xì)
Junit
//AssertionError里沒有expected和actual的信息
assertTrue(expected.contains(actual));
java.lang.AssertionError at ...
Hamcrest
assertThat(actual, containsString(expected));
java.lang.AssertionError:
Expected: a string containing "abc"
got: "def"
Assert 條件更加靈活
可以將多個(gè)assert條件通過anyOf() (或), allOf()(與)組合在一起
assertThat("test", anyOf(is("test2"), containsString("te")));
More Info
http://www.vogella.com/tutorials/Hamcrest/article.html
Mockito
testCompile "org.mockito:mockito-core:2.2.0"
Mock所需要測試的類,可以指定方法的返回值
兩種方式創(chuàng)建mock
1.注解,這種方式必須添加MockitoRule
public class MockitoTest {
@Mock
MyDatabase databaseMock;
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void testQuery() {
ClassToTest t = new ClassToTest(databaseMock);
boolean check = t.query("* from t");
assertTrue(check);
verify(databaseMock).query("* from t");
}
}
2.代碼創(chuàng)建mock
@Test
public void test1() {
// create mock
MyClass test = Mockito.mock(MyClass.class);
// define return value for method getUniqueId()
when(test.getUniqueId()).thenReturn(43);
// use mock in test....
assertEquals(test.getUniqueId(), 43);
}
驗(yàn)證方法的調(diào)用
@Test
public void testVerify() {
// create and configure mock
MyClass test = Mockito.mock(MyClass.class);
when(test.getUniqueId()).thenReturn(43);
// call method testing on the mock with parameter 12
test.testing(12);
test.getUniqueId();
test.getUniqueId();
// now check if method testing was called with the parameter 12
verify(test).testing(Matchers.eq(12));
// was the method called twice?
verify(test, times(2)).getUniqueId();
// other alternatives for verifiying the number of method calls for a method
verify(mock, never()).someMethod("never called");
verify(mock, atLeastOnce()).someMethod("called at least once");
verify(mock, atLeast(2)).someMethod("called at least twice");
verify(mock, times(5)).someMethod("called five times");
verify(mock, atMost(3)).someMethod("called at most 3 times");
}
Spy vs Mock
對真正對象的包裝,所有調(diào)用會(huì)調(diào)用真正對象的方法,但是會(huì)記錄方法調(diào)用的信息
調(diào)用方法:@Spy or Mockito.spy()
@Test
public void whenSpyingOnList_thenCorrect() {
List<String> list = new ArrayList<String>();
List<String> spyList = Mockito.spy(list);
spyList.add("one");
spyList.add("two");
Mockito.verify(spyList).add("one");
Mockito.verify(spyList).add("two");
assertEquals(2, spyList.size());
}
Spy也可以覆蓋真正對象的方法:
@Test
public void whenStubASpy_thenStubbed() {
List<String> list = new ArrayList<String>();
List<String> spyList = Mockito.spy(list);
assertEquals(0, spyList.size());
Mockito.doReturn(100).when(spyList).size();
assertEquals(100, spyList.size());
}
The mock simply creates a bare-bones shell instance of the Class, entirely instrumented to track interactions
with it.
On the other hand, the spy will wrap an existing instance. It will still behave in the same way as the normal
instance – the only difference is that it will also be instrumented to track all the interactions with it.
依賴注入
為了方便測試,一個(gè)類對外部類的依賴需要通過某種方式傳入,而不是在類的內(nèi)部創(chuàng)建依賴
public class MyClass {
Foo foo;
Boo boo;
public MyClass() {
foo = new Foo(); //依賴在內(nèi)部創(chuàng)建,無法mock
boo = new Boo();
}
}
public class MyClass {
Foo foo;
Boo boo;
public MyClass(Foo foo, Boo boo) {
this.foo = foo; //依賴注入,測試MyClass時(shí)可以傳入Mock的foo和boo
this.boo = boo;
}
}
More Info
http://www.baeldung.com/mockito-spy
http://www.vogella.com/tutorials/Mockito/article.html
Robolectric
testCompile "org.robolectric:robolectric:3.1.1"
Robolectric is designed to allow you to test Android applications on the JVM based on the JUnit 4 framework. Robolectric is a framework that allows you to write unit tests and run them on a desktop JVM while still using Android API. Robolectric mocks part of the Android framework contained in the android.jar file. Robolectric provides also implementations for the methods while the standard Android unit testing support throws exceptions for all Android methods.
This enables you to run your Android tests in your continuous integration environment without any additional setup. Robolectric supports resource handling, e.g., inflation of views. You can also use the findViewById() to search in a view
如何使用
在test之前使用RunWith注解
@RunWith(RobolectricGradleTestRunner.class)
在@Config中配置測試參數(shù)
@Config(sdk = Build.VERSION_CODES.JELLY_BEAN(default 16),
application = CustomApplication.class,
manifest = "some/build/path/AndroidManifest.xml")
在測試運(yùn)行時(shí),robolectric根據(jù)你要測試的api level從maven倉庫中拉取對應(yīng)android api實(shí)現(xiàn)jar包,如果你的機(jī)器上網(wǎng)絡(luò)不太好,可以將這些jar先下載到本地,然后啟用offline模式
android {
testOptions {
unitTests.all {
systemProperty 'robolectric.offline', 'true'
systemProperty 'robolectric.dependency.dir', "somewhere you place your jar"
}
}
}

示例代碼:
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class MyActivityTest {
private MainActivity activity;
@Test
public void shouldHaveHappySmiles() throws Exception {
String hello = new MainActivity().getResources().getString(
R.string.hello_world);
assertThat(hello, equalTo("Hello world!"));
}
@Before
public void setup() {
activity = Robolectric.buildActivity(MainActivity.class)
.create().get();
}
@Test
public void checkActivityNotNull() throws Exception {
assertNotNull(activity);
}
@Test
public void buttonClickShouldStartNewActivity() throws Exception
{
Button button = (Button) activity.findViewById( R.id.button2 );
button.performClick();
Intent intent = Shadows.shadowOf(activity).peekNextStartedActivity();
assertEquals(SecondActivity.class.getCanonicalName(), intent.getComponent().getClassName());
}
@Test
public void testButtonClick() throws Exception {
MainActivity activity = Robolectric.buildActivity(MainActivity.class)
.create().get();
Button view = (Button) activity.findViewById(R.id.button1);
assertNotNull(view);
view.performClick();
assertThat(ShadowToast.getTextOfLatestToast(), equalTo("Lala") );
}
}