測試是確保軟體品質的關鍵環節。透過系統性的測試,我們能明確驗證開發成果的正確性,保障系統穩定性。良好的測試覆蓋率讓後續維護更輕鬆,重構代碼時能快速發現潛在問題。此外,將測試整合進 CI/CD 流程(如 Phabricator differential 提交時強制執行單元測試),可從流程層面保障遠端代碼庫的穩定性。
[被測試方法]_[測試條件]_[預期結果]。private 方法,透過驗證呼叫它的 public 介面即可;同理,getter/setter 等瑣碎代碼可略過。將游標置於目標類別名稱上,按下 Alt + Enter 選擇 Create Test 即可快速生成測試骨架。執行預設快捷鍵為 Ctrl + Shift + R。
直接在開發機 JVM 上運行,適用於無 Android 依賴或僅需輕量 Android 庫(如 android.jar)的純邏輯驗證。執行指令:./gradlew check。
預設位於 src/test/java。若有多 flavor 或 build type,對應目錄為 src/test[FlavorName]/java(如 src/testMyFlavor/java)。
dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:4.8.0'
}
| 註解 | 用途 |
|---|---|
@Test | 標記測試方法 |
@Test(expected = Exception.class) | 預期拋出特定異常 |
@Test(timeout = 100) | 超時失敗(毫秒) |
@Before / @After | 每個測試前/後的準備與清理 |
@BeforeClass / @AfterClass | 所有測試前/後的一次性昂貴操作(如資料庫連線) |
@Ignore | 暫時禁用測試 |
@FixMethodOrder(MethodSorters.NAME_ASCENDING) | 強制按方法名字母序執行 |
Assume.assumeFalse(condition) | 條件滿足時跳過測試 |
需在真機或模擬器上運行,適用於涉及 Android SDK、UI 互動、組件整合(Service、ContentProvider 等)的場景。執行指令:./gradlew connectedAndroidTest。
src/androidTest/java
dependencies {
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
androidTestImplementation 'org.hamcrest:hamcrest-library:2.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
}
Espresso 提供流暢的 API 進行 UI 互動驗證:
// 點擊並驗證顯示
onView(withId(R.id.my_view)).perform(click()).check(matches(isDisplayed()));
// 驗證文字開頭為 "ABC" 的 View 未啟用
onView(withText(startsWith("ABC"))).check(matches(not(isEnabled())));
// 模擬返回鍵
pressBack();
// 驗證按鈕文字
onView(withId(R.id.button)).check(matches(withText("Start new activity")));
// 輸入文字並關閉鍵盤
onView(withId(R.id.inputField)).perform(typeText("NewText"), closeSoftKeyboard());
// 清除輸入框
onView(withId(R.id.inputField)).perform(clearText());
@RunWith(AndroidJUnit4.class)
public class SecondActivityTest {
@Rule
public ActivityTestRule<SecondActivity> rule = new ActivityTestRule<>(
SecondActivity.class, true, false); // 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 預設同步等待 UI 執行緒閒置,但對於 AsyncTask、IntentService 等後台任務,需實作 IdlingResource 手動同步。建議關閉開發者選項中的動畫縮放以減少干擾。
public class IntentServiceIdlingResource implements IdlingResource {
private ResourceCallback callback;
private final Context context;
public IntentServiceIdlingResource(Context context) { this.context = context; }
@Override public String getName() { return getClass().getName(); }
@Override public void registerIdleTransitionCallback(ResourceCallback cb) { this.callback = cb; }
@Override public boolean isIdleNow() {
boolean idle = !isIntentServiceRunning();
if (idle && callback != null) callback.onTransitionToIdle();
return idle;
}
private boolean isIntentServiceRunning() {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningServiceInfo info : am.getRunningServices(Integer.MAX_VALUE)) {
if (MyIntentService.class.getName().equals(info.service.getClassName())) return true;
}
return false;
}
}
// 測試中註冊
@Before public void setUp() {
idlingResource = new IntentServiceIdlingResource(
InstrumentationRegistry.getInstrumentation().getTargetContext());
Espresso.registerIdlingResources(idlingResource);
}
@After public void tearDown() {
Espresso.unregisterIdlingResources(idlingResource);
}
@Test public void testAsyncFlow() {
onView(withId(R.id.action_settings)).perform(click()); // 觸發 IntentService
// 此處阻塞直到 IdlingResource 回報閒置
onView(withText("Broadcast")).check(matches(notNullValue()));
}
public static Matcher<View> withItemHint(final String hintText) {
checkArgument(hintText != null);
return withItemHint(is(hintText));
}
public static Matcher<View> withItemHint(final Matcher<String> matcher) {
checkNotNull(matcher);
return new BoundedMatcher<View, EditText>(EditText.class) {
@Override protected boolean matchesSafely(EditText view) {
return matcher.matches(view.getHint().toString());
}
@Override public void describeTo(Description desc) {
desc.appendText("with item hint: ").appendDescriptionOf(matcher);
}
};
}
// 使用
onView(withItemHint("test")).check(matches(isDisplayed()));
square/assertj-android 提供鏈式斷言,大幅提升可讀性:
assertThat(view).isGone();
assertThat(test.getName()).isEqualTo("Frodo");
assertThat(test).isNotEqualTo(test1).isIn(testList);
assertThat(test.getName()).startsWith("Fro").endsWith("do").isEqualToIgnoringCase("frodo");
assertThat(testList).hasSize(2).contains(test, test1).doesNotContain(test2);
assertThat(testList).extracting("name").contains("Frodo", "Sauron").doesNotContain("Jacks");
核心匹配器:allOf、anyOf、not、equalTo、hasItem、hasSize、containsString、closeTo(浮點數比較)等。支援自定義 TypeSafeMatcher 實現正則等複雜匹配。
核心能力:mock() 建立替身、when(...).thenReturn(...) 存根行為、verify() 驗證互動、ArgumentCaptor 捕獲參數。spy() 可部分模擬真實對象。
MyClass mock = mock(MyClass.class);
when(mock.getUniqueId()).thenReturn(43);
when(mock.compareTo(anyInt())).thenReturn(43);
doThrow(new IOException()).when(mock).close();
verify(mock, times(2)).getUniqueId();
// 捕獲可變參數
ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class);
mock.doSomething("a", "b");
verify(mock).doSomething(captor.capture());
assertThat(captor.getAllValues()).contains("a", "b");
靜態方法模擬可用 PowerMock(@RunWith(PowerMockRunner.class) + @PrepareForTest),或採用包裝類轉為實例方法後用 Mockito。
透過 Shadow Classes 模擬 Android 框架,讓依賴 Handler、Context 等系統 API 的測試直接在 JVM 跑完,極大縮短反饋迴路。
模擬真實用戶操作序列,適合端到端的視覺驗證,但仍需裝置/模擬器運行。
LinkedIn 開源,自動處理鎖屏、動畫、Wi-Fi 等環境干擾,降低 UI 測試不穩定率,獲 Google 官方推薦。
Jake Wharton 提出的 Robot 模式:將「如何操作 UI」封裝於 Robot 類別,測試案例僅關注「驗證什麼業務邏輯」,實現測試邏輯與 View/Presenter 解耦。
優勢:測試碼可讀性高、維護成本低、複用性強。