首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Android 單元測試與模擬測試完整指南

     編輯:Java 綜合教學

為什麼需要測試?

測試是確保軟體品質的關鍵環節。透過系統性的測試,我們能明確驗證開發成果的正確性,保障系統穩定性。良好的測試覆蓋率讓後續維護更輕鬆,重構代碼時能快速發現潛在問題。此外,將測試整合進 CI/CD 流程(如 Phabricator differential 提交時強制執行單元測試),可從流程層面保障遠端代碼庫的穩定性。

測試內容與基本規範

測試什麼?

  • 一般單元測試:針對異常情況驗證、效能基準測試。
  • 模擬測試:依據需求模擬真實用戶場景,驗證 UI 回饋、顯示邏輯及依賴系統架構的組件行為。

編寫測試的核心原則

  • 可讀性優先:採用表達力強的方法命名,建議遵循 RSpec 風格,格式為 [被測試方法]_[測試條件]_[預期結果]。
  • 避免邏輯分支:單一測試方法中不應包含 if/else、迴圈等流程控制;若必要,請拆分為多個獨立測試用例。
  • 聚焦核心驗證:專注於輸出結果驗證(如操作後的顯示內容、數值變化),避免過度測試內部實現細節。
  • 關注執行效能:Android Studio 預設輸出測試耗時,需留意效能退化。
  • 黑盒測試私有方法:無需直接測試 private 方法,透過驗證呼叫它的 public 介面即可;同理,getter/setter 等瑣碎代碼可略過。
  • 測試獨立性:每個測試方法應無執行順序依賴,避免測試間的時序耦合。

在 Android Studio 中建立與執行測試

將游標置於目標類別名稱上,按下 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)。

核心依賴(Google 官方建議)

dependencies {
    testImplementation 'junit:junit:4.13.2'
    testImplementation 'org.mockito:mockito-core:4.8.0'
}

JUnit 4 常用註解速查

註解用途
@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

核心依賴(Google 官方建議)

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 UI 測試實戰

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());

啟動帶 Extra 的 Activity

@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")));
    }
}

異步操作處理:IdlingResource

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()));
}

自定義 Matcher 擴展驗證能力

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()));

進階測試工具生態

AssertJ Android — 更易讀的斷言

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");

Hamcrest — 豐富的匹配器組合

核心匹配器:allOf、anyOf、not、equalTo、hasItem、hasSize、containsString、closeTo(浮點數比較)等。支援自定義 TypeSafeMatcher 實現正則等複雜匹配。

Mockito — 模擬依賴與行為驗證

核心能力: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。

Robolectric — 本機跑儀器測試

透過 Shadow Classes 模擬 Android 框架,讓依賴 Handler、Context 等系統 API 的測試直接在 JVM 跑完,極大縮短反饋迴路。

Robotium — 黑盒 UI 流程測試

模擬真實用戶操作序列,適合端到端的視覺驗證,但仍需裝置/模擬器運行。

Test Butler — 穩定測試環境

LinkedIn 開源,自動處理鎖屏、動畫、Wi-Fi 等環境干擾,降低 UI 測試不穩定率,獲 Google 官方推薦。

架構模式:Instrumentation Testing Robots

Jake Wharton 提出的 Robot 模式:將「如何操作 UI」封裝於 Robot 類別,測試案例僅關注「驗證什麼業務邏輯」,實現測試邏輯與 View/Presenter 解耦。
優勢:測試碼可讀性高、維護成本低、複用性強。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved