Android TDD 系列 — 21 Android MVVM 架構的單元測試
介紹完了 DataBinding、ViewModel、LiveData,可以開始來寫MVVM的單元測試了。
測試 ProductViewModel.getProduct
先看ProductionCode,getProduct 負責跟Repository要資料後將取得的資料放到LiveData。與MVP的差異在於Presenter的在取得資料後,會呼叫view的callback去要求View應該做什麼事。而在MVVM的ViewModel只負責將資料放到LiveData。而View有沒有正確的顯示資料跟ViewModel就沒有關係了。
ProductViewModel的Production Code
class ProductViewModel(private val productRepository: IProductRepository) : ViewModel(){ var productName: MutableLiveData<String> = MutableLiveData()
var productDesc: MutableLiveData<String> = MutableLiveData()
var productPrice: MutableLiveData<Int> = MutableLiveData()
var productItems: MutableLiveData<String> = MutableLiveData() fun getProduct(productId: String) {
productRepository.getProduct(productId, object : IProductRepository.LoadProductCallback {
override fun onProductResult(productResponse: ProductResponse) {
productName.value = productResponse.name
productDesc.value = productResponse.desc
productPrice.value = productResponse.price
}
})
}
}
build.Gradle加上
testImplementation "android.arch.core:core-testing:$archLifecycleVersion"
建立ProductViewModelTest,加上InstantTaskExecutorRule
class ProductViewModelTest { @get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
}
在測試的初始化,建立被測試物件viewModel、模擬物件repository及驗證用的ProductResponse。
class ProductViewModelTest { @get:Rule
var instantExecutorRule = InstantTaskExecutorRule() @Mock
lateinit var repository: IProductRepository
private var productResponse = ProductResponse()
private lateinit var viewModel: ProductViewModel @Before
fun setUp() {
MockitoAnnotations.initMocks(this) productResponse.id = "pixel3"
productResponse.name = "Google Pixel 3"
productResponse.price = 27000
productResponse.desc = "Desc" viewModel = ProductViewModel(repository)
}
}
驗證getProductTest
1.驗證是否有呼叫IProductRepository.getProduct
2.驗證是否有改變LiveData
的值。
@Test
fun getProductTest() {
val productId = "pixel3"
viewModel.getProduct(productId) val loadProductCallbackCaptor = argumentCaptor<IProductRepository.LoadProductCallback>() //驗證是否有呼叫IProductRepository.getProduct
verify(repository).getProduct(eq(productId), capture(loadProductCallbackCaptor)) //將callback攔截下載並指定productResponse的值。
loadProductCallbackCaptor.value.onProductResult(productResponse) Assert.assertEquals(productResponse.name, viewModel.productName.value)
Assert.assertEquals(productResponse.desc, viewModel.productDesc.value)
Assert.assertEquals(productResponse.price, viewModel.productPrice.value)
}
購買成功、購買失敗的測試
Production Code
1.購買成功將buySuccessText
指定為Event("購買成功")
2.購買失敗將buyFailText
指定為Event("購買失敗")
fun buy(view: View) {
val productId = productId.value ?: ""
val numbers = (productItems.value ?: "0").toInt() productRepository.buy(productId, numbers, object : IProductRepository.BuyProductCallback {
override fun onBuyResult(isSuccess: Boolean) {
if (isSuccess) {
buySuccessText.value = Event("購買成功")
} else {
alertText.value = Event("購買失敗")
}
}
})
}
對於viewModel的buy,當購買成功時,只需要知道buySuccessText會被改變。MVVM的好處,ViewModel只要改變LiveData,而有沒有真的在Snack跳出buySuccessText就是View的事情了。這裡我們用一個模糊的比對,只要buySuccessText不為null,就算測試成功。而不是直接去比對字串為相等,因為這會導至測試變得脆弱。如果之後購買成功的文字有改時,測試就會失敗。
購買成功的測試
1.驗證是否有呼叫IProductRepository.getProduct
2.驗證購買成功是否有設定buySuccessText
@Test
fun buySuccess() {
val buyProductCallbackCaptor = argumentCaptor<IProductRepository.BuyProductCallback>() val productId = "pixel3"
val items = 3
val productViewModel = ProductViewModel(repository)
productViewModel.productId.value = productId
productViewModel.productItems.value = items.toString() productViewModel.buy() //驗證是否有呼叫IProductRepository.getProduct
verify(repository).buy(eq(productId), eq(items), capture(buyProductCallbackCaptor)) buyProductCallbackCaptor.value.onBuyResult(true) Assert.assertTrue(productViewModel.buySuccessText.value != null)
}
購買失敗的測試
1.驗證是否有呼叫IProductRepository.getProduct
2.驗證購買失敗是否有設定alertText
@Test
fun buyFail() {
val buyProductCallbackCaptor = argumentCaptor<IProductRepository.BuyProductCallback>() val productId = "pixel3"
val items = 11
val productViewModel = ProductViewModel(repository)
productViewModel.productId.value = productId
productViewModel.productItems.value = items.toString() productViewModel.buy() //驗證是否有呼叫IProductRepository.getProduct
verify(repository).buy(eq(productId), eq(items), capture(buyProductCallbackCaptor)) buyProductCallbackCaptor.value.onBuyResult(false) Assert.assertTrue(productViewModel.alertText.value != null)
}
以上就是MVVM 的單元測試,拆成Model、View、ViewModel之後,是不是就更好寫測試了。在MVP的架構時,Presenter仍擁有View。而在MVVM,ViewModel就完全跟View就無關了,兩者完全沒有依賴。
為了更方便單元測試,我們使用了依賴注入的方式,也導至在Activity需要建立Repository注入ViewModel,這樣的做法有點麻煩,且Activity出現Repository也怪怪的,下一篇將介紹Kotlin的依賴注入框架Koin來簡化注入的方式。
範例下載:
https://github.com/evanchen76/mvvmlivedatasample
Android TDD 系列
下一篇:22 依賴注入框架Koin