Hilt 測試最佳實踐 | MAD Skills

Android開發者發表於2021-10-21

本文是 MAD Skills 系列 中有關 Hilt 的第二篇文章。這次我們聚焦如何使用 Hilt 編寫測試,以及一些需要注意的最佳實踐。

如果您更喜歡通過視訊瞭解此內容,可以 點選此處 檢視.

Hilt 的測試理念

由於 Hilt 是一個有特定處理原則的框架,所以它的測試 API 是基於一些特定目標建立的。瞭解 Hilt 用於測試的方法有助於您使用和理解它的 API。如需進一步瞭解測試理念的更多資訊,請參閱: Hilt 的測試理念

Hilt 測試 API 的一個核心目標,便是在測試中減少對不必要的虛假或模擬物件的使用,同時儘可能地使用真實物件。真實物件可以增加測試的覆蓋率,並且相對於虛假或模擬的物件也更經得起日後的變化。當真實物件執行開銷昂貴的任務 (例如 IO 操作) 時,虛假或模擬的物件便很有用。但它們經常被過度使用,很多人會用它來解決那些在概念上完全可以在測試中完成的問題。

一個相關例子是,如果使用了 Dagger 而沒有用 Hilt, 測試時就會非常麻煩。為測試設定 Dagger 元件可能需要大量的工作和模板程式碼,但如果不用 Dagger 並手動例項化物件又會導致過度使用模擬物件。下面讓我們看看為什麼會這樣。

手動例項化 (測試時不使用 Hilt)

讓我們通過一個例子來了解為什麼在測試中手動例項化物件會導致模擬物件的過度使用。

在下面的程式碼中,我們對含有一些依賴項的 EventManager 類進行測試。由於不想為這樣簡單的測試配置 Dagger 元件,所以我們直接手動例項化該物件。

class EventManager @Inject constructor(
    dataModel: DataModel,
    errorHandler: ErrorHandler
) {}

@RunWith(JUnit4::class)
class EventManagerTest {
  @Test
  fun testEventManager() {
    val eventManager = EventManager(dataModel, errorHandler)

    // 測試程式碼
  }
}

一開始,由於我們只是像 Dagger 一樣呼叫了建構函式,所以一切看起來都十分簡單。但當我們需要解決如何獲得 DataModel與 ErrorHandler 例項的問題時,麻煩就來了:

@RunWith(JUnit4::class)
class EventManagerTest {
  @Test
  fun testEventManager() {
    // 呃...changeNotifier 要怎麼處理?
    val dataModel = DataModel(changeNotifier)  
    val errorHandler = ErrorHandler(errorConfig)

    val eventManager = EventManager(dataModel, errorHandler)

    // 測試程式碼
  }
}

我們也可以直接例項化這些物件,但是如果這些物件同樣包含依賴,那麼繼續下去可能會過於深入。在進行實際測試前,我們最終可能會呼叫很多個建構函式。另外,這些建構函式的呼叫也會使測試變得脆弱。任何一個建構函式的改變都會破壞測試,即使它們在生產環境中沒有破壞任何內容。本應為 "無操作" 的更改,例如在 @Inject 建構函式中改變引數順序,或者通過 @Inject 建構函式為某個類新增依賴,都會破壞測試且難以對其進行更新。

為了避免這一問題,人們經常只是模擬對 DataModel 與 ErrorHandler 的依賴。但這同樣是一個問題,因為引入這些模擬物件並不是為了避免測試中的任何昂貴操作,而只是為了處理測試的設定模板程式碼而已。

使用 Hilt 進行測試

使用 Hilt 時,它會幫您設定好 Dagger 元件,這樣您便無需手動例項化物件,也能避免在測試中配置 Dagger 而產生模版程式碼。更多測試內容請參閱 完整的測試文件

若要在您的測試中配置 Hilt,您需要:

對於第三步來說,如何使用 HiltTestApplication 取決於您測試的型別:

  • 對於 Robolectric 測試,請查閱 文件
  • 對於插樁測試,請查閱 文件

配置完成後,您便可以為您的測試新增 @Inject 欄位來訪問繫結。這些欄位會在您呼叫 HiltAndroidRuleinject() 後賦值,所以您可以在您的 setup 方法中完成這一操作。

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class EventManagerTest {

  @get:Rule
  val rule = HiltAndroidRule(this)

  @Inject
  lateinit var eventManager: EventManager

  @Before
  fun setup() {
    rule.inject(this)
  }

  @Test
  fun testEventManager() {
    // 使用注入的 eventManager 進行測試
  }
}

需要注意的是,注入的物件必須來自 SingletonComponent。如果您需要來自 ActivityComponentFragmentComponent 的物件,則需要使用常規 Android 測試 API 來建立一個 Activity 或 Fragment 並從中獲取依賴。

隨後您便可以開始編寫測試了。您所注入的欄位 (在本例中是我們的 EventManager 類) 將會像在生產環境中一樣由 Dagger 為您構造。您無需擔心管理依賴所產生的任何模版程式碼。

TestInstallIn

當您在測試中遇到需要替換依賴的情況,比如真實物件會做諸如呼叫伺服器這樣的昂貴操作時,您可以使用 TestInstallIn 來進行替換。

不過您無法直接在 Hilt 中替換某個繫結,但您可以通過 TestInstallIn 替換模組。TestInstallIn 的工作形式與 InstallIn 類似,不同之處在於它還允許您指定需要被替換的模組。被替換的模組將不會被 Hilt 使用,而任何加入 TestInstallIn 模組的繫結都會被使用。與 InstallIn 模組相似,TestInstallIn 模組會應用於所有依賴它們的測試 (例如 Gradle 模組中的所有測試)。

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [BackendModule::class]
)
object FakeBackendModule {

  @Singleton
  @Provides
  fun provideBackend(): BackendClient {
    return FakeBackend.inMemoryBackendBuilder(
              /* ...虛擬後臺資料... */
           ).build()
  }
}

UninstallModules

當您遇到需要只在單個測試中替換依賴的情況時,可以使用 UninstallModules。您可以直接在測試上新增 UninstallModules 註解,並通過它指定 Hilt 不應使用哪些模組。

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
@UninstallModules(BackendModule::class)
class DataFetcherTest {

  @BindValue
  val fakeBackend = FakeBackend.inMemoryBackendBuilder(...).build()

  ...
}

在測試中,您可以使用 @BindValue 或通過定義巢狀元件來直接新增繫結。

TestInstallIn vs UninstallModules

您也許會疑惑: 應該使用兩者中的哪一個呢?下面我們對兩者進行一些對比:

TestInstallIn

  • 應用於全域性
  • 便於配置
  • 利於提升構建速度

UninstallModules

  • 只針對單個測試
  • 非常靈活
  • 不利於構建速度

通常,我們推薦從 TestInstallIn 開始,因為它有利於提升構建速度。當您確實需要單獨的配置時,仍然可以使用 UninstallModules,但是我們建議您僅在特別需要時謹慎使用。

TestInstallIn/UninstallModules 影響構建速度的原因

對於每個用於測試的不同模組組,Hilt 需要建立一組新的元件。這些元件最終可能會非常大,當您依賴了大量生產程式碼中的模組時尤其如此。

△ 為不同模組組生成的元件

△ 為不同模組組生成的元件

UninstallModules 的每次使用都會新增一組必須被構建的新元件,元件的數量可能會基於您的測試數量而成倍增加 。而由於 TestInstallIn 作用於全域性,所以它會加入一組元件的預設集合,而該集合可以在多個測試中共享。如果您可以通過改變測試而使其不必使用 UninstallModules,那麼就可以減少一組需要構建的元件。

但有時測試還是需要使用 UninstallModules。沒關係!只要注意權衡並儘可能預設使用 TestInstallIn 即可。

測試依賴

另一種可以加快測試構建速度的方式是減少拉入測試的模組和入口點。這個部分會在每次使用 UninstallModules 時翻倍。有時候,您測試的實際覆蓋範圍很小,卻可能依賴了所有的生產環境程式碼。由於 Hilt 在編譯時無法確定您將在執行時測試什麼,因此 Hilt 必須構建一個可以通過您的依賴關係找到每個模組和入口點的元件。這些模組和入口點可能會很多,並且可能會產生很大的 Dagger 元件,從而導致構建時間的增加。

如果您可以減少這些依賴項,那麼新增的 UninstallModules 可能不會產生太多消耗,從而可以讓您在配置測試時更為靈活。

一種減少依賴的方法是組織您的 Gradle 模組,您可以在此過程中將大量測試從主應用的 Gradle 模組分離至依賴庫的 Gradle 模組中,從而減少所需的依賴。

△ 儘可能將測試組織到依賴庫 Gradle 模組中

△ 儘可能將測試組織到依賴庫 Gradle 模組中

組織 Hilt 模組

要時刻記得考慮如何組織您的 Hilt,這也有助於您編寫測試。我們常常能夠看到十分巨大且擁有許多繫結的 Dagger 模組,但是對於 Hilt 來說,由於您需要替換整個模組而不是單獨的繫結,那些可以做許多事的大型模組只會讓測試變得更加困難。

在使用 Hilt 模組時,您需要儘可能地保持它們的單一目的性,為此甚至可以只加入一個公開的繫結。這有助於提高可讀性,並在需要時可以更簡單的在測試中替換它們。

更多資源

應用上述這些實踐內容並瞭解更多其中權衡的思路,將會幫助您更輕鬆的編寫 Hilt 測試。對於其中的一些 API 來說,您選擇哪種方式很大程度上取決於您應用、測試以及構建系統的設定方式。

有關使用 Hilt 進行測試的更多資訊,請查閱:

以上便是有關 Hilt 測試的全部內容,我們即將推出更多 MAD Skills 文章,敬請關注。

歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!

相關文章