
本文是 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,您需要:
- 為您的測(cè)試添加 @HiltAndroidTest 注解
- 添加測(cè)試規(guī)則 HiltAndroidRule
- 為 Application 類使用 HiltTestApplication
對(duì)于第三步來(lái)說,如何使用 HiltTestApplication 取決于您測(cè)試的類型:
配置完成后,您便可以為您的測(cè)試添加 @Inject 字段來(lái)訪問綁定。這些字段會(huì)在您調(diào)用 HiltAndroidRule 的 inject() 后賦值,所以您可以在您的 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)自 ActivityComponent 或 FragmentComponent 的對(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 模塊中
組織 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ì)我們非常重要,感謝您的支持!