編輯:Android資訊
測試驅動式編程(Test-Driven-Development)在RoR中已經是非常普遍的開發模式,是一種十分可靠、優秀的編程思想,可是在Android領域中這塊還沒有普及,今天主要聊聊Android中的單元測試與模擬測試及其常用的一些庫。
[測試的方法]_[測試的條件]_[符合預期的結果]
。private
的方法,將private
方法當做黑盒內部組件,測試對其引用的public
方法即可;不考慮測試瑣碎的代碼,如getter
或者setter
。ALT + ENTER
Create Test
control + shift + R (Android Studio 默認執行單元測試快捷鍵)。
直接在開發機上面進行運行測試。
在沒有依賴或者僅僅只需要簡單的Android庫依賴的情況下,有限考慮使用該類單元測試。
./gradlew check
如果是對應不同的flavor或者是build type,直接在test後面加上對應後綴(如對應名為
myFlavor
的單元測試代碼,應該放在src/testMyFlavor/java
下面)。
src/test/java
dependencies { // Required -- JUnit 4 framework,用於單元測試,google官方推薦 testCompile 'junit:junit:4.12' // Optional -- Mockito framework,用於模擬架構,google官方推薦 testCompile 'org.mockito:mockito-core:1.10.19' }
@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
,就不執行對應方法
需要運行在Android設備或者虛擬機上的測試。
主要用於測試: 單元(Android SDK層引用關系的相關的單元測試)、UI、應用組件集成測試(Service、Content Provider等)。
./gradlew connectedAndroidTest
src/androidTest/java
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 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' }
需要模擬Android系統環境。
谷歌官方提供用於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" 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" 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
之後。
// 通過實現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中的事務,理解關閉自己 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,然後比對下是否相同 return matcherText.matches(editTextField.getHint().toString()); } }; } // 使用 onView(withItemHint("test")).check(matches(isDisplayed()));
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中 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");
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 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));
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
字符串匹配
// 自定義 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"));
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() 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"); }
可以使用 PowerMock:
org.powermock:powermock-api-mockito:(version)
&org.powermock:powermock-module-junit4:(version)
(ForPowerMockRunner.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(); } }
Robolectric
讓模擬測試直接在開發機上完成,而不需要在Android系統上。所有需要使用到系統架構庫的,如(Handler
、HandlerThread
)都需要使用Robolectric,或者進行模擬測試。
主要是解決模擬測試中耗時的缺陷,模擬測試需要安裝以及跑在Android系統上,也就是需要在Android虛擬機或者設備上面,所以十分的耗時。基本上每次來來回回都需要幾分鐘時間。針對這類問題,業界其實已經有了一個現成的解決方案: Pivotal實驗室推出的Robolectric。通過使用Robolectrict模擬Android系統核心庫的Shadow Classes
的方式,我們可以像寫本地測試一樣寫這類測試,並且直接運行在工作環境的JVM上,十分方便。
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事件 activity.findViewById(R.id.login).performClick(); Intent expectedIntent = new Intent(activity, LoginActivity.class); // 在activity之後,啟動的Activity是否是LoginActivity assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent); } }
通過模擬用戶的操作的行為事件流進行測試,這類測試無法避免需要在虛擬機或者設備上面運行的。是一些用戶操作流程與視覺顯示強相關的很好的選擇。
linkedin/test-butler
避免設備/模擬器系統或者環境的錯誤,導致測試的失敗。
通常我們在進行UI測試的時候,會遇到由於模擬器或者設備的錯誤,如系統的crash、ANR、或是未預期的Wifi、CPU罷工,或者是鎖屏,這些外再環境因素導致測試不過。Test-Butler引入就是避免這些環境因素導致UI測試不過。
該庫被谷歌官方推薦過,並且收到谷歌工程師的Review。
Instrumentation Testing Robots – Jake Wharton
假如我們需要測試: 發送 $42 到 “[email protected]”,然後驗證是否成功。
在寫真正的UI測試的時候,只需要關注要測試什麼,而不需要關注需要怎麼測試,換句話說就是讓測試邏輯與View或Presenter解耦,而與數據產生關系。
首先通過封裝一個Robot去處理How的部分:
然後在寫測試的時候,只關注需要測試什麼:
最終的思想原理
一 IntentService介紹 IntentService定義的三個基本點:是什麼?怎麼用?如何work? 官方解釋如下: //IntentService定義
前言 兩周前我開始用 Unity 開發一個叫 SkyBlocks 的 Android 游戲。游戲已經在 Google Play 上架了,如果你有時間可以下載來玩一
對於我這樣一個Android初級開發者來說,自定義View一直是一個遙不可及的東西,每次看到別人做的特別漂亮的控件,自己心裡那個癢癢啊,可是又生性懶惰,自己不肯努
模式的定義 適配器模式把一個類的接口變換成客戶端所期待的另一種接口,從而使原本因接口不匹配而無法在一起工作的兩個類能夠在一起工作。 使用場景 用電源接口做例子,筆