爲何須要測試?javascript
2. 測什麼?html
3. 須要注意java
[測試的方法]_[測試的條件]_[符合預期的結果]
。private
的方法,將private
方法當作黑盒內部組件,測試對其引用的public
方法便可;不考慮測試瑣碎的代碼,如getter
或者setter
。4. 建立測試android
ALT + ENTER
Create Test
control + shift + R (Android Studio 默認執行單元測試快捷鍵)。git
1. 本地單元測試github
直接在開發機上面進行運行測試。
在沒有依賴或者僅僅只須要簡單的Android庫依賴的狀況下,有限考慮使用該類單元測試。數據庫
./gradlew check
(1)代碼存儲 express
若是是對應不一樣的flavor或者是build type,直接在test後面加上對應後綴(如對應名爲myFlavor
的單元測試代碼,應該放在src/testMyFlavor/java
下面)。api
src/test/java
dependencies { // Required -- JUnit 4 framework,用於單元測試,google官方推薦 testCompile 'junit:junit:4.12' // Optional -- Mockito framework,用於模擬架構,google官方推薦 // http://www.manongjc.com/article/1546.html testCompile 'org.mockito:mockito-core:1.10.19' }
(3)JUnit數組
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 ,就不執行對應方法 |
2. 模擬測試
須要運行在Android設備或者虛擬機上的測試。
主要用於測試: 單元(Android SDK層引用關係的相關的單元測試)、UI、應用組件集成測試(Service、Content Provider等)。
./gradlew connectedAndroidTest
(1)代碼存儲:
src/androidTest/java
(2)Google官方推薦引用
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' // Optional -- Hamcrest library androidTestCompile 'org.hamcrest:hamcrest-library:1.3' // Optional -- UI testing with Espresso // http://www.manongjc.com/article/1546.html androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' // Optional -- UI testing with UI Automator androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1' }
(3)常見的UI測試
須要模擬Android系統環境。
(4)Espresso
谷歌官方提供用於UI交互測試
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; // 對於Id爲R.id.my_view的View: 觸發點擊,檢測是否顯示 onView(withId(R.id.my_view)).perform(click()) .check(matches(isDisplayed())); // 對於文本打頭是"ABC"的View: 檢測是否沒有Enable onView(withText(startsWith("ABC"))).check(matches(not(isEnabled())); // 按返回鍵 pressBack(); // 對於Id爲R.id.button的View: 檢測內容是不是"Start new activity" // http://www.manongjc.com/article/1537.html onView(withId(R.id.button)).check(matches(withText(("Start new activity")))); // 對於Id爲R.id.viewId的View: 檢測內容是否不包含"YYZZ" onView(withId(R.id.viewId)).check(matches(withText(not(containsString("YYZZ"))))); // 對於Id爲R.id.inputField的View: 輸入"NewText",而後關閉軟鍵盤 onView(withId(R.id.inputField)).perform(typeText("NewText"), closeSoftKeyboard()); // 對於Id爲R.id.inputField的View: 清除內容 onView(withId(R.id.inputField)).perform(clearText());
Activity
的Intent
@RunWith(AndroidJUnit4.class) public class SecondActivityTest { @Rule public ActivityTestRule<SecondActivity> rule = new ActivityTestRule(SecondActivity.class, true, // 這個參數爲false,不讓SecondActivity自動啓動 // 若是爲true,將會在全部@Before以前啓動,在最後一個@After以後關閉 false); @Test public void demonstrateIntentPrep() { Intent intent = new Intent(); intent.putExtra("EXTRA", "Test"); // 啓動SecondActivity並傳入intent rule.launchActivity(intent); // 對於Id爲R.id.display的View: 檢測內容是不是"Text" // http://www.manongjc.com/article/1532.html onView(withId(R.id.display)).check(matches(withText("Test"))); } }
(5)異步交互
建議關閉設備中」設置->開發者選項中」的動畫,由於這些動畫可能會是的Espresso在檢測異步任務的時候產生混淆: 窗口動畫縮放(Window animation scale)、過渡動畫縮放(Transition animation scale)、動畫程序時長縮放(Animator duration scale)。
針對AsyncTask
,在測試的時候,如觸發點擊事件之後拋了一個AsyncTask
任務,在測試的時候直接onView(withId(R.id.update)).perform(click())
,而後直接進行檢測,此時的檢測就是在AsyncTask#onPostExecute
以後。
// 經過實現IdlingResource,block住當非空閒的時候,當空閒時進行檢測,非空閒的這段時間處理異步事情 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() { // 是不是空閒 // 若是IntentService 沒有在運行,就說明異步任務結束,IntentService特質就是啓動之後處理完Intent中的事務,理解關閉本身 // http://www.manongjc.com/article/1531.html boolean idle = !isIntentServiceRunning(); if (idle && resourceCallback != null) { // 回調告知異步任務結束 resourceCallback.onTransitionToIdle(); } return idle; } private boolean isIntentServiceRunning() { ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); // Get all running services List<ActivityManager.RunningServiceInfo> runningServices = manager.getRunningServices(Integer.MAX_VALUE); // check if our is running for (ActivityManager.RunningServiceInfo info : runningServices) { if (MyIntentService.class.getName().equals(info.service.getClassName())) { return true; } } return false; } } // 使用IntentServiceIdlingResource來測試,MyIntentService服務啓動結束這個異步事務,以後的結果。 @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() { // MainActivity中點擊R.id.action_settings這個View的時候,會啓動MyIntentService onView(withId(R.id.action_settings)).perform(click()); // 這時候IntentServiceIdlingResource#isIdleNow會返回false,由於MyIntentService服務啓動了 // 這個狀況下,這裏會block住............. // 直到IntentServiceIdlingResource#isIdleNow返回true,而且回調了IntentServiceIdlingResource#onTransitionToIdle // 這個狀況下,繼續執行,這時咱們就能夠測試異步結束之後的狀況了。 onView(withText("Broadcast")).check(matches(notNullValue())); } }
// 定義 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) { // 取出hint,而後比對下是否相同 // http://www.manongjc.com/article/1524.html return matcherText.matches(editTextField.getHint().toString()); } }; } // 使用 onView(withItemHint("test")).check(matches(isDisplayed()));
1. AssertJ Android
square/assertj-android
極大的提升可讀性。
import static org.assertj.core.api.Assertions.*; // 斷言: view是GONE的 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); // 斷言: test.getName()等於"Frodo" assertThat(test.getName()).isEqualTo("Frodo"); // 斷言: test不等於test1而且在testList中 // http://www.manongjc.com/article/1519.html assertThat(test).isNotEqualTo(test1) .isIn(testList); // 斷言: test.getName()的字符串,是由"Fro"打頭,以"do"結尾,忽略大小寫會等於"frodo" assertThat(test.getName()).startsWith("Fro") .endsWith("do") .isEqualToIgnoringCase("frodo"); // 斷言: testList有2個數據,包含test,test1,不包含test2 assertThat(list).hasSize(2) .contains(test, test1) .doesNotContain(test2); // 斷言: 提取testList隊列中全部數據中的成員變量名爲name的變量,而且包含name爲"Frodo"與"Sauron" // 而且不包含name爲"Jacks" assertThat(testList).extracting("name") .contains("Frodo", "Sauron") .doesNotContain("Jacks");
2. Hamcrest
JavaHamcrest
經過已有的通配方法,快速的對代碼條件進行測試
org.hamcrest:hamcrest-junit:(version)
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.equalTo; // 斷言: a等於b assertThat(a, equalTo(b)); assertThat(a, is(equalTo(b))); assertThat(a, is(b)); // 斷言: a不等於b assertThat(actual, is(not(equalTo(b)))); List<Integer> list = Arrays.asList(5, 2, 4); // 斷言: list有3個數據 assertThat(list, hasSize(3)); // 斷言: list中有5,2,4,而且順序也一致 assertThat(list, contains(5, 2, 4)); // 斷言: list中包含5,2,4 assertThat(list, containsInAnyOrder(2, 4, 5)); // 斷言: list中的每個數據都大於1 // http://www.manongjc.com/article/1507.html assertThat(list, everyItem(greaterThan(1))); // 斷言: fellowship中包含有成員變量"race",而且其值不是ORC assertThat(fellowship, everyItem(hasProperty("race", is(not((ORC)))))); // 斷言: object1中與object2相同的成員變量都是相同的值 assertThat(object1, samePropertyValuesAs(object2)); Integer[] ints = new Integer[] { 7, 5, 12, 16 }; // 斷言: 數組中包含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 |
字符串匹配 |
(2)自定義匹配器
// 自定義 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)
// import如相關類 import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; // 建立一個Mock的對象 MyClass test = mock(MyClass.class); // 當調用test.getUniqueId()的時候返回43 when(test.getUniqueId()).thenReturn(43); // 當調用test.compareTo()傳入任意的Int值都返回43 when(test.compareTo(anyInt())).thenReturn(43); // 當調用test.compareTo()傳入的是Target.class類型對象時返回43 when(test.compareTo(isA(Target.class))).thenReturn(43); // 當調用test.close()的時候,拋IOException異常 doThrow(new IOException()).when(test).close(); // 當調用test.execute()的時候,什麼都不作 doNothing().when(test).execute(); // 驗證是否調用了兩次test.getUniqueId() // http://www.manongjc.com/article/1503.html verify(test, times(2)).getUniqueId(); // 驗證是否沒有調用過test.getUniqueId() verify(test, never()).getUniqueId(); // 驗證是否至少調用過兩次test.getUniqueId() verify(test, atLeast(2)).getUniqueId(); // 驗證是否最多調用過三次test.getUniqueId() verify(test, atMost(3)).getUniqueId(); // 驗證是否這樣調用過:test.query("test string") verify(test).query("test string"); // 經過Mockito.spy() 封裝List對象並返回將其mock的spy對象 List list = new LinkedList(); List spy = spy(list); // 指定spy.get(0)返回"foo" doReturn("foo").when(spy).get(0); assertEquals("foo", spy.get(0));
對訪問方法時,傳入參數進行快照
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()); // 須要特別注意,若是是可變數組(vargars)參數,如方法 test.doSomething(String... params) // 此時是使用ArgumentCaptor<String>,而非ArgumentCaptor<String[]> ArgumentCaptor<String> varArgs = ArgumentCaptor.forClass(String.class); test.doSomething("param-1", "param-2"); verify(test).doSomething(varArgs.capture()); // 這裏直接使用getAllValues()而非getValue(),來獲取可變數組參數的全部傳入參數 assertThat(varArgs.getAllValues()).contains("param-1", "param-2"); }
(1)對於靜態的方法的Mock:
可使用 PowerMock:
org.powermock:powermock-api-mockito:(version) & org.powermock:powermock-module-junit4:(version)(For PowerMockRunner.class)
@RunWith(PowerMockRunner.class) @PrepareForTest({StaticClass1.class, StaticClass2.class}) public class MyTest { @Test public void testSomething() { // mock完靜態類之後,默認全部的方法都不作任何事情 mockStatic(StaticClass1.class); when(StaticClass1.getStaticMethod()).andReturn("anything"); // 驗證是否StaticClass1.getStaticMethod()這個方法被調用了一次 verifyStatic(time(1)); StaticClass1.getStaticMethod(); when(StaticClass1.getStaticMethod()).andReturn("what ever"); // 驗證是否StaticClass2.getStaticMethod()這個方法被至少調用了一次 verifyStatic(atLeastOnce()); StaticClass2.getStaticMethod(); // 經過任何參數建立File的實力,都直接返回fileInstance對象 whenNew(File.class).withAnyArguments().thenReturn(fileInstance); } }
或者是封裝爲非靜態,而後用Mockito:
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)模擬用戶操做,事件流測試。
@RunWith(RobolectricTestRunner.class) @Config(constants = BuildConfig.class) public class MyActivityTest{
@Test public void doSomethingTests(){ // 獲取Application對象 Application application = RuntimeEnvironment.application; // 啓動WelcomeActivity WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class); // 觸發activity中Id爲R.id.login的View的click事件 // http://www.manongjc.com/article/1502.html activity.findViewById(R.id.login).performClick(); Intent expectedIntent = new Intent(activity, LoginActivity.class); // 在activity以後,啓動的Activity是不是LoginActivity assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent); } }
經過模擬用戶的操做的行爲事件流進行測試,這類測試沒法避免須要在虛擬機或者設備上面運行的。是一些用戶操做流程與視覺顯示強相關的很好的選擇。
6. Test Butler
linkedin/test-butler
避免設備/模擬器系統或者環境的錯誤,致使測試的失敗。
一般咱們在進行UI測試的時候,會遇到因爲模擬器或者設備的錯誤,如系統的crash、ANR、或是未預期的Wifi、CPU罷工,或者是鎖屏,這些外再環境因素致使測試不過。Test-Butler引入就是避免這些環境因素致使UI測試不過。
該庫被谷歌官方推薦過,而且收到谷歌工程師的Review。
1. Android Robots
Instrumentation Testing Robots – Jake Wharton
假如咱們須要測試: 發送 $42 到 「foo@bar.com」,而後驗證是否成功。
(2)Robot思想
在寫真正的UI測試的時候,只須要關注要測試什麼,而不須要關注須要怎麼測試,換句話說就是讓測試邏輯與View或Presenter解耦,而與數據產生關係。
首先經過封裝一個Robot去處理How的部分:
而後在寫測試的時候,只關注須要測試什麼:
最終的思想原理