Android 單元測試基礎

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第4天,點選檢視活動詳情

本文參考自:http://developer.android.com/training/testing/fundamentals?hl=zh-cn#testing-pyramid

在大多數公司的實際的 Android 開發中,大多數都是使用黑盒測試,直接執行專案從使用者操作流程來進行測試工作的。本文主要介紹單元測試在 Android 中的應用和一些常見的框架。

Android 單元測試基礎

測試是應用程式開發過程中不可或缺的一部分。通過對應用程式持續執行測試,您可以在釋出應用程式之前驗證其正確性、功能行為和可用性。 移動應用程式很複雜,必須在許多環境中正常工作。因此,有許多型別的測試。

例如,根據測試目標的不同,可以劃分為不同的型別: - 功能測試:App 做了它應該做的事情了嗎? - 效能測試:能否快速有效的完成工作? - 無障礙測試:是否適用於無障礙服務? - 相容性測試:App 是否適用於所有裝置和 API 級別?

測試也可根據測試範圍的大小和隔離程度劃分為不同的級別: - 單元測試或小型測試僅驗證應用程式的一小部分內容,例如方法或類。 - 端到端測試或大型測試同時驗證應用程度的較大的模組,例如整個螢幕或使用者輸入流。 - 中型測試介於兩者之間,並檢查兩個或多個模組之間的整合。

image.png

有很多方法可以對測試進行分類。但是,對於應用程式開發人員來說,最重要的區別是測試執行的位置。

測試金字塔(如下圖所示)說明了應用應如何包含三類測試(即小型、中型和大型測試):

pyramid.png

  • 小型測試是指單元測試,用於驗證應用的行為,一次驗證一個類或一個方法。
  • 中型測試是指整合測試,用於驗證模組之間的互動。
  • 大型測試是指 End-to-End 測試,用於驗證跨越了應用的多個模組的使用者操作流程。

沿著金字塔逐級向上,從小型測試到大型測試,各類測試的保真度逐級提高,但維護和除錯工作所需的執行時間和工作量也逐級增加。因此,您編寫的單元測試應多於整合測試,整合測試應多於端到端測試。雖然各類測試的比例可能會因應用的用例不同而異,但我們通常建議各類測試所佔比例如下:小型測試佔 70%,中型測試佔 20%,大型測試佔 10%

如需詳細瞭解 Android 測試金字塔,請觀看 2017 年 Google I/O 大會的 Android 平臺上的測試驅動型開發會議視訊(從 1 分 51 秒開始)。

小型測試

小型測試應該是高度集中的單元測試,能夠詳盡地驗證應用中每個類的功能和職責。測試 Android 應用程式,可以通過在一臺 Android 裝置或計算機上進行,而根據執行環境可以分為兩種: - 插樁測試:在 Android 裝置上執行,可以是模擬器也可以是物理裝置。該應用程式通過注入命令與讀取狀態的測試應用程式一起構建和安裝。通常用於 UI 測試,會啟動應用程式然後與之進行互動。 - 本地測試:本地測試會在開發機或者伺服器上執行,也稱之為 host-side tests 。這種測試執行速度更快,並且會將測試物件與應用程式的其他部分隔離開。

image.png

並非所有單元測試都是本地的,也並非所有端到端測試都在裝置上執行。例如:

  • 大型本地測試:可以使用在本地執行的安卓模擬器,比如Robolectric
  • 小型插樁測試:您可以驗證您的程式碼是否適用於框架功能,例如 SQLite 資料庫。您可以在多個裝置上執行此測試以檢查與多個 SQLite 版本的整合。

例如,以下程式碼片段演示瞭如何在 檢測的 UI 測試中與 UI 互動,該測試單擊一個元素並驗證另一個元素是否顯示。

```kotlin // When the Continue button is clicked onView(withText("Continue"))     .perform(click())

// Then the Welcome screen is displayed onView(withText("Welcome"))     .check(matches(isDisplayed())) ```

此程式碼段顯示了 ViewModel 單元測試的一部分(本地測試): ```kotlin val viewModel = MyViewModel(myFakeDataRepository)

viewModel.loadData()

// Then it should be exposing data assertTrue(viewModel.data != null) ```

本地單元測試

本地單元測試是直接在自己的開發計算機上直接執行測試程式碼,無需依賴 Android 相關的環境。本質上是使用本地 JVM 而不是 Android 裝置執行程式碼。

本地測試可以更快的評估程式的邏輯,但是無法與 Android 框架進行互動會很大程度限制可測試的內容。一個典型的例子是,當我們需要 Android App 的 Context 時,就很難直接獲取到。(使用第三方框架即可,後續會說明)。

預設情況下,本地單元測試的原始檔放在 module-name/src/test/. 當您使用 Android Studio 建立新專案時,此目錄已經存在。

為專案配置測試依賴,單元測試主要是使用 Junit 測試框架提供的標準 API。 groovy dependencies {   // Required -- JUnit 4 framework   testImplementation "junit:junit:$jUnitVersion"   // Optional -- Robolectric environment   testImplementation "androidx.test:core:$androidXTestVersion"   // Optional -- Mockito framework   testImplementation "org.mockito:mockito-core:$mockitoVersion"   // Optional -- mockito-kotlin   testImplementation "org.mockito.kotlin:mockito-kotlin:$mockitoKotlinVersion"   // Optional -- Mockk framework   testImplementation "io.mockk:mockk:$mockkVersion" } 這裡用到了幾個三方框架: - Robolectric :用於模擬 Android 環境。 - Mockito :一個小型庫,提供輔助函式以在 Kotlin 中使用 Mockito。 - Mockk :kotlin 的 mock 框架。

一個簡單的本地測試:

kotlin class ExampleUnitTest { @Test fun addition_isCorrect() { assertEquals(4, 2 + 2) } } 通過斷言方法 assertTrue 可以檢查返回的結果是否符合預期。當前直接執行是會執行成功的,展示效果如下:

image-20220607153910441.png

而如果想構建一個失敗的單元測試,可以將方法中的 assertEquals 修改為: kotlin assertEquals(4, 2 + 1) 這樣執行失敗後,IDE 會有錯誤日誌:

image-20220607154241733.png

在文章的前面有一個問題是本地單元測試不依賴 Android 環境,無法訪問 Android 框架中的內容,比如通過 Context 去獲取字串資源 : kotlin context.resources.getString(...) 使用 Mockable Android 庫和 MockitoMockK等模擬框架,您可以在單元測試中對 Android 類的模擬行為進行程式設計。 執行以下步驟: 1. 新增 Mockito 和 MockK 的依賴 2. 在測試類上添加註解 @RunWith(MockitoJUnitRunner.class) 。此註釋告訴 Mockito 測試執行程式驗證您對框架的使用是否正確並簡化了模擬物件的初始化。 3. 要為 Android 依賴項建立模擬物件,請@Mock在欄位宣告之前添加註釋。 4. 編寫測試程式碼

```kotlin @RunWith(MockitoJUnitRunner::class) class MockedContextTest {

@Mock   private lateinit var mockContext: Context

@Test   fun readStringFromContext_LocalizedString() {     val mockContext = mock {         on { getString(R.string.name_label) } doReturn "FAKE_STRING"     }     val myObjectUnderTest = ClassUnderTest(mockContext)     val result: String = myObjectUnderTest.getName()     assertEquals(result, FAKE_STRING)   } } ``` 也可以通過 Robolectric 的 API 來獲取一個 Activity 作為 Context 物件,注意它們的註解中的測試執行程式不同:

```java @RunWith(RobolectricTestRunner.class) public class MyActivityTest { @Test public void clickingButton_shouldChangeMessage() { MyActivity activity = Robolectric.setupActivity(MyActivity.class);

String text = activity.resources.getString(R.string.app_name);

assertThat(text).isEqualTo("Robolectric");

} } ```

插樁單元測試

插樁單元測試執行在 Android 裝置上,無論是模擬器還是真實物理裝置都可以。它們會反映更多實際執行會發生的情況,但速度會慢很多。

預設情況下,插樁單元測試的原始檔放在 module-name/src/androidTest/. 當您使用 Android Studio 建立新專案時,此目錄已經存在。

官方建議僅在必須針對真實裝置的行為進行測試的情況才使用這種單元測試。

在開始之前,您應該新增 AndroidX 測試 API,它允許您為您的應用程式快速構建和執行經過檢測的測試程式碼。AndroidX Test 包括一個 JUnit 4 測試AndroidJUnitRunner執行程式,以及用於功能 UI 測試的 API,例如EspressoUI AutomatorCompose 測試

設定依賴內容: groovy dependencies {     androidTestImplementation "androidx.test:runner:$androidXTestVersion"     androidTestImplementation "androidx.test:rules:$androidXTestVersion"     // Optional -- UI testing with Espresso     androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"     // Optional -- UI testing with UI Automator     androidTestImplementation "androidx.test.uiautomator:uiautomator:$uiAutomatorVersion"     // Optional -- UI testing with Compose     androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" } 插樁測試類的測試執行器應該是一個 Junit4 類,需要在測試類上添加註解: kotlin @RunWith(AndroidJUnit4::class) class LogHistoryAndroidUnitTest 在插樁測試中,如果需要訪問 Android 框架中的內容,也可通過上述第三方框架 Robolectric 、Mockito 等實現,也可直接使用 androidx.test 提供的 API 訪問 Android 框架中的內容,例如: kotlin @RunWith(AndroidJUnit4::class) class TasksViewModelTest { @Test fun addNewTask_setsNewTaskEvent() { val tasksViewModel = TasksViewModel(ApplicationProvider.getApplicationContext()) tasksViewModel.addNewTask() } }

其他資源

通過命令列執行插樁測試:http://developer.android.com/studio/test/command-line