Hilt 測(cè)試最佳實(shí)踐 | MAD Skills

image

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

如果您更喜歡通過視頻了解此內(nèi)容,可以 點(diǎn)擊此處 查看.

Hilt 的測(cè)試?yán)砟?/strong>

由于 Hilt 是一個(gè)有特定處理原則的框架,所以它的測(cè)試 API 是基于一些特定目標(biāo)創(chuàng)建的。了解 Hilt 用于測(cè)試的方法有助于您使用和理解它的 API。如需進(jìn)一步了解測(cè)試?yán)砟畹母嘈畔?,?qǐng)參閱: Hilt 的測(cè)試?yán)砟?/a>。

Hilt 測(cè)試 API 的一個(gè)核心目標(biāo),便是在測(cè)試中減少對(duì)不必要的虛假或模擬對(duì)象的使用,同時(shí)盡可能地使用真實(shí)對(duì)象。真實(shí)對(duì)象可以增加測(cè)試的覆蓋率,并且相對(duì)于虛假或模擬的對(duì)象也更經(jīng)得起日后的變化。當(dāng)真實(shí)對(duì)象執(zhí)行開銷昂貴的任務(wù) (例如 IO 操作) 時(shí),虛假或模擬的對(duì)象便很有用。但它們經(jīng)常被過度使用,很多人會(huì)用它來(lái)解決那些在概念上完全可以在測(cè)試中完成的問題。

一個(gè)相關(guān)例子是,如果使用了 Dagger 而沒有用 Hilt, 測(cè)試時(shí)就會(huì)非常麻煩。為測(cè)試設(shè)置 Dagger 組件可能需要大量的工作和模板代碼,但如果不用 Dagger 并手動(dòng)實(shí)例化對(duì)象又會(huì)導(dǎo)致過度使用模擬對(duì)象。下面讓我們看看為什么會(huì)這樣。

手動(dòng)實(shí)例化 (測(cè)試時(shí)不使用 Hilt)

讓我們通過一個(gè)例子來(lái)了解為什么在測(cè)試中手動(dòng)實(shí)例化對(duì)象會(huì)導(dǎo)致模擬對(duì)象的過度使用。

在下面的代碼中,我們對(duì)含有一些依賴項(xiàng)的 EventManager 類進(jìn)行測(cè)試。由于不想為這樣簡(jiǎn)單的測(cè)試配置 Dagger 組件,所以我們直接手動(dòng)實(shí)例化該對(duì)象。

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

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

    // 測(cè)試代碼
  }
}

一開始,由于我們只是像 Dagger 一樣調(diào)用了構(gòu)造函數(shù),所以一切看起來(lái)都十分簡(jiǎn)單。但當(dāng)我們需要解決如何獲得 DataModel與 ErrorHandler 實(shí)例的問題時(shí),麻煩就來(lái)了:

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

    val eventManager = EventManager(dataModel, errorHandler)

    // 測(cè)試代碼
  }
}

我們也可以直接實(shí)例化這些對(duì)象,但是如果這些對(duì)象同樣包含依賴,那么繼續(xù)下去可能會(huì)過于深入。在進(jìn)行實(shí)際測(cè)試前,我們最終可能會(huì)調(diào)用很多個(gè)構(gòu)造函數(shù)。另外,這些構(gòu)造函數(shù)的調(diào)用也會(huì)使測(cè)試變得脆弱。任何一個(gè)構(gòu)造函數(shù)的改變都會(huì)破壞測(cè)試,即使它們?cè)谏a(chǎn)環(huán)境中沒有破壞任何內(nèi)容。本應(yīng)為 "無(wú)操作" 的更改,例如在 @Inject 構(gòu)造函數(shù)中改變參數(shù)順序,或者通過 @Inject 構(gòu)造函數(shù)為某個(gè)類添加依賴,都會(huì)破壞測(cè)試且難以對(duì)其進(jìn)行更新。

為了避免這一問題,人們經(jīng)常只是模擬對(duì) DataModel 與 ErrorHandler 的依賴。但這同樣是一個(gè)問題,因?yàn)橐脒@些模擬對(duì)象并不是為了避免測(cè)試中的任何昂貴操作,而只是為了處理測(cè)試的設(shè)置模板代碼而已。

使用 Hilt 進(jìn)行測(cè)試

使用 Hilt 時(shí),它會(huì)幫您設(shè)置好 Dagger 組件,這樣您便無(wú)需手動(dòng)實(shí)例化對(duì)象,也能避免在測(cè)試中配置 Dagger 而產(chǎn)生模版代碼。更多測(cè)試內(nèi)容請(qǐng)參閱 完整的測(cè)試文檔。

若要在您的測(cè)試中配置 Hilt,您需要:

對(duì)于第三步來(lái)說,如何使用 HiltTestApplication 取決于您測(cè)試的類型:

  • 對(duì)于 Robolectric 測(cè)試,請(qǐng)查閱 文檔。
  • 對(duì)于插樁測(cè)試,請(qǐng)查閱 文檔。

配置完成后,您便可以為您的測(cè)試添加 @Inject 字段來(lái)訪問綁定。這些字段會(huì)在您調(diào)用 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 進(jìn)行測(cè)試
  }
}

需要注意的是,注入的對(duì)象必須來(lái)自 SingletonComponent。如果您需要來(lái)自 ActivityComponentFragmentComponent 的對(duì)象,則需要使用常規(guī) Android 測(cè)試 API 來(lái)創(chuàng)建一個(gè) Activity 或 Fragment 并從中獲取依賴。

隨后您便可以開始編寫測(cè)試了。您所注入的字段 (在本例中是我們的 EventManager 類) 將會(huì)像在生產(chǎn)環(huán)境中一樣由 Dagger 為您構(gòu)造。您無(wú)需擔(dān)心管理依賴所產(chǎn)生的任何模版代碼。

TestInstallIn

當(dāng)您在測(cè)試中遇到需要替換依賴的情況,比如真實(shí)對(duì)象會(huì)做諸如調(diào)用服務(wù)器這樣的昂貴操作時(shí),您可以使用 TestInstallIn 來(lái)進(jìn)行替換。

不過您無(wú)法直接在 Hilt 中替換某個(gè)綁定,但您可以通過 TestInstallIn 替換模塊。TestInstallIn 的工作形式與 InstallIn 類似,不同之處在于它還允許您指定需要被替換的模塊。被替換的模塊將不會(huì)被 Hilt 使用,而任何加入 TestInstallIn 模塊的綁定都會(huì)被使用。與 InstallIn 模塊相似,TestInstallIn 模塊會(huì)應(yīng)用于所有依賴它們的測(cè)試 (例如 Gradle 模塊中的所有測(cè)試)。

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

  @Singleton
  @Provides
  fun provideBackend(): BackendClient {
    return FakeBackend.inMemoryBackendBuilder(
              /* ...虛擬后臺(tái)數(shù)據(jù)... */
           ).build()
  }
}

UninstallModules

當(dāng)您遇到需要只在單個(gè)測(cè)試中替換依賴的情況時(shí),可以使用 UninstallModules。您可以直接在測(cè)試上添加 UninstallModules 注解,并通過它指定 Hilt 不應(yīng)使用哪些模塊。

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

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

  ...
}

在測(cè)試中,您可以使用 @BindValue 或通過定義嵌套組件來(lái)直接添加綁定。

TestInstallIn vs UninstallModules

您也許會(huì)疑惑: 應(yīng)該使用兩者中的哪一個(gè)呢?下面我們對(duì)兩者進(jìn)行一些對(duì)比:

TestInstallIn

  • 應(yīng)用于全局
  • 便于配置
  • 利于提升構(gòu)建速度

UninstallModules

  • 只針對(duì)單個(gè)測(cè)試
  • 非常靈活
  • 不利于構(gòu)建速度

通常,我們推薦從 TestInstallIn 開始,因?yàn)樗欣谔嵘龢?gòu)建速度。當(dāng)您確實(shí)需要單獨(dú)的配置時(shí),仍然可以使用 UninstallModules,但是我們建議您僅在特別需要時(shí)謹(jǐn)慎使用。

TestInstallIn/UninstallModules 影響構(gòu)建速度的原因

對(duì)于每個(gè)用于測(cè)試的不同模塊組,Hilt 需要?jiǎng)?chuàng)建一組新的組件。這些組件最終可能會(huì)非常大,當(dāng)您依賴了大量生產(chǎn)代碼中的模塊時(shí)尤其如此。

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

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

UninstallModules 的每次使用都會(huì)添加一組必須被構(gòu)建的新組件,組件的數(shù)量可能會(huì)基于您的測(cè)試數(shù)量而成倍增加 。而由于 TestInstallIn 作用于全局,所以它會(huì)加入一組組件的默認(rèn)集合,而該集合可以在多個(gè)測(cè)試中共享。如果您可以通過改變測(cè)試而使其不必使用 UninstallModules,那么就可以減少一組需要構(gòu)建的組件。

但有時(shí)測(cè)試還是需要使用 UninstallModules。沒關(guān)系!只要注意權(quán)衡并盡可能默認(rèn)使用 TestInstallIn 即可。

測(cè)試依賴

另一種可以加快測(cè)試構(gòu)建速度的方式是減少拉入測(cè)試的模塊和入口點(diǎn)。這個(gè)部分會(huì)在每次使用 UninstallModules 時(shí)翻倍。有時(shí)候,您測(cè)試的實(shí)際覆蓋范圍很小,卻可能依賴了所有的生產(chǎn)環(huán)境代碼。由于 Hilt 在編譯時(shí)無(wú)法確定您將在運(yùn)行時(shí)測(cè)試什么,因此 Hilt 必須構(gòu)建一個(gè)可以通過您的依賴關(guān)系找到每個(gè)模塊和入口點(diǎn)的組件。這些模塊和入口點(diǎn)可能會(huì)很多,并且可能會(huì)產(chǎn)生很大的 Dagger 組件,從而導(dǎo)致構(gòu)建時(shí)間的增加。

如果您可以減少這些依賴項(xiàng),那么新增的 UninstallModules 可能不會(huì)產(chǎn)生太多消耗,從而可以讓您在配置測(cè)試時(shí)更為靈活。

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

△ 盡可能將測(cè)試組織到依賴庫(kù) Gradle 模塊中

△ 盡可能將測(cè)試組織到依賴庫(kù) Gradle 模塊中

組織 Hilt 模塊

要時(shí)刻記得考慮如何組織您的 Hilt,這也有助于您編寫測(cè)試。我們常常能夠看到十分巨大且擁有許多綁定的 Dagger 模塊,但是對(duì)于 Hilt 來(lái)說,由于您需要替換整個(gè)模塊而不是單獨(dú)的綁定,那些可以做許多事的大型模塊只會(huì)讓測(cè)試變得更加困難。

在使用 Hilt 模塊時(shí),您需要盡可能地保持它們的單一目的性,為此甚至可以只加入一個(gè)公開的綁定。這有助于提高可讀性,并在需要時(shí)可以更簡(jiǎn)單的在測(cè)試中替換它們。

更多資源

應(yīng)用上述這些實(shí)踐內(nèi)容并了解更多其中權(quán)衡的思路,將會(huì)幫助您更輕松的編寫 Hilt 測(cè)試。對(duì)于其中的一些 API 來(lái)說,您選擇哪種方式很大程度上取決于您應(yīng)用、測(cè)試以及構(gòu)建系統(tǒng)的設(shè)置方式。

有關(guān)使用 Hilt 進(jìn)行測(cè)試的更多信息,請(qǐng)查閱:

以上便是有關(guān) Hilt 測(cè)試的全部?jī)?nèi)容,我們即將推出更多 MAD Skills 文章,敬請(qǐng)關(guān)注。

歡迎您 點(diǎn)擊這里 向我們提交反饋,或分享您喜歡的內(nèi)容、發(fā)現(xiàn)的問題。您的反饋對(duì)我們非常重要,感謝您的支持!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容