延續上一篇的架構採用MVP後,我們就來看這個範例該怎麼撰寫單元測試。
build.gradle
testImplementation "org.mockito:mockito-core:2.8.47"
新增一個MockitoKotlinHelpers.kt,用來解決Kotlin在Mock的問題。
fun <T> eq(obj: T): T = Mockito.eq<T>(obj)
/**
* Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when
* null is returned.
*/
fun <T> any(): T = Mockito.any<T>()
/**
* Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
* when null is returned.
*/
fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
/**
* Helper function for creating an argumentCaptor in kotlin.
*/
inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
ArgumentCaptor.forClass(T::class.java)
先來看在ProductionCode的ProductPresenter.getProduct做了什麼事。
1.呼叫productRepository.getProduct取得產品資料
2.呼叫view.onGetResult回傳productResponse
所以我們的測試必須驗證這兩個項目
override fun getProduct(productId: String) {
productRepository.getProduct(productId, object : IProductRepository.LoadProductCallback {
override fun onProductResult(productResponse: ProductResponse) {
view.onGetResult(productResponse)
}
})
}
新增ProducePresenterTest
1.建立被測試物件ProductPresenter
2.建立Mock IProductRepsitory
3.建立Mock IProductView
會需要步驟2、3的 IProductRepsitory、IProductView是因為待會需要驗證是否有呼叫IProductRepository與IProductView的Callback。
class ProductPresenterTest {
private lateinit var presenter: ProductContract.IProductPresenter
private var productResponse = ProductResponse()
@Mock
private lateinit var repository: IProductRepository
@Mock
private lateinit var productView: ProductContract.IProductView
}
在setupPresenter,將物件初始化
1.使用 MockitoAnnotations.initMocks(this)初始化Mock。
2.被測試物件ProductPresenter初始化
3.ProductResponse初始化,這是用來測試用的資料
@Before
fun setupPresenter() {
MockitoAnnotations.initMocks(this)
presenter = ProductPresenter(productView, repository)
productResponse.id = "pixel3"
productResponse.name = "Google Pixel 3"
productResponse.price = 27000
productResponse.desc = "Desc"
}
開始寫getProduct的測試。
@Test
fun getProductTest() {
val productId = "pixel3"
//呼叫SUT
presenter.getProduct(productId)
val loadProductCallbackCaptor = argumentCaptor<IProductRepository.LoadProductCallback>()
//驗證是否有呼叫IProductRepository.getProduct
Mockito.verify<IProductRepository>(repository).getProduct(eq(productId), capture(loadProductCallbackCaptor))
//將callback攔截下載並指定productResponse的值。
loadProductCallbackCaptor.value.onProductResult(productResponse)
//驗證是否有呼叫View.onGetResult及是否傳入productResponse
Mockito.verify(productView).onGetResult(productResponse)
}
在這個測試中,argumentCaptor
用來取得callback,準備攔截並給值。
val loadProductCallbackCaptor = argumentCaptor<IProductRepository.LoadProductCallback>()
使用verity來驗證是否有呼叫repository.getProduct,使用eq()方法驗證repository.getProduct傳入的參數是否正確。
第2個參數則帶入loadProductCallbackCaptor,可以攔截callback並指定回傳的值。
verify<IProductRepository>(repository).getProduct(eq(productId), capture(loadProductCallbackCaptor))
將callback攔截下並指定productResponse的值。
loadProductCallbackCaptor.value.onProductResult(productResponse)
最後則是驗證是否有呼叫View的Callback
verify(productView).onGetResult(productResponse)
在MVP的架構,Presenter的測試非常重要,負責呼叫Repository與將資料處理過後呼叫View的Callback。對於Presenter的getProduct在取得資料後,只要呼叫View的Callback就做到Presenter的職責了,而畫面有沒有正確的顯示資料則是Activity該處理的事。在Presenter,你不會有處理View的行為,只會透過在Presenter初始化時傳進來的IProductView,要求View該做什麼事。
在ProductionCode,一樣來看getProduct做了什麼事。
1.跟ProductAPI取得產品資料
2.呼叫Callback回傳資料
class ProductRepository(private val productAPI: IProductAPI) : IProductRepository {
override fun getProduct(productId: String, loadProductCallback: IProductRepository.LoadProductCallback) {
productAPI.getProduct(productId, object : IProductAPI.LoadAPICallBack {
override fun onGetResult(productResponse: ProductResponse) {
loadProductCallback.onProductResult(productResponse)
}
})
}
}
新增ProductRepositoryTest
1.建立被測試物件ProductRepository
2.建立Mock IProductAPI
3.建立Mock IProductRepository.loadProductCallback
4.初始化setup
class ProductRepositoryTest {
private lateinit var repository: IProductRepository
private var productResponse = ProductResponse()
@Mock
private lateinit var productAPI: IProductAPI
@Mock
private lateinit var repositoryCallback : IProductRepository.LoadProductCallback
@Before
fun setupPresenter() {
MockitoAnnotations.initMocks(this)
repository = ProductRepository(productAPI)
productResponse.id = "pixel3"
productResponse.name = "Google Pixel 3"
productResponse.price = 27000
productResponse.desc = "Desc"
}
}
開始寫getProduct的測試
@Test
fun getProductTest() {
//驗證跟Repository取得資料
val productId = "pixel3"
repository.getProduct(productId, repositoryCallback)
//驗證是否有呼叫IProductAPI.getProduct
val productAPICallbackCaptor = argumentCaptor<IProductAPI.LoadAPICallBack>()
Mockito.verify<IProductAPI>(productAPI).getProduct(any(), capture(productAPICallbackCaptor))
//將callback攔截下載並指定productResponse的值。
productAPICallbackCaptor.value.onGetResult(productResponse)
//驗證是否有呼叫Callback
Mockito.verify(repositoryCallback).onProductResult(productResponse)
}
在上一篇給了大家的一個練習,按下「購買」後。如購買成功Toast「購買成功」,購買失敗則Alert「購買失敗」。
在ProductContract
1.IProductPresenter加上buy
2.在IProductView加上 onBuySuccess、onBuyFail
class ProductContract {
interface IProductPresenter {
fun getProduct(productId: String)
fun buy(productId: String, numbers: Int)
}
interface IProductView {
fun onGetResult(productResponse: ProductResponse)
fun onBuySuccess()
fun onBuyFail()
}
}
在ProductPresenter.buy實作,依Repository回傳的購買成功或失敗呼叫對映的callBack
override fun buy(productId: String, numbers: Int) {
productRepository.buy(productId, numbers, object : IProductRepository.BuyProductCallback {
override fun onBuyResult(isSuccess: Boolean) {
if (isSuccess) {
view.onBuySuccess()
} else {
view.onBuyFail()
}
}
})
}
在Activity則實作購買成功與失敗要做的事。可以看到Activity不負責什麼情況視為購買成功與購買失敗。只接受Presenter告訴View現在該呈現購買成功的畫面還是購買失敗的畫面。Activity上的程式碼就會非常的簡單,幾乎是沒有邏輯。
Activity
override fun onBuySuccess() {
Toast.makeText(this, "購買成功", Toast.LENGTH_LONG).show();
}
override fun onBuyFail() {
val builder = AlertDialog.Builder(this)
builder.setMessage("購買失敗").setTitle("錯誤")
builder.show()
}
購買成功的測試
@Test
fun buySuccessTest() {
val buyProductCallbackCaptor = argumentCaptor<IProductRepository.BuyProductCallback>()
val productId = "pixel3"
val items = 3
presenter.buy(productId, items)
verify(repository).buy(eq(productId), eq(items), capture(buyProductCallbackCaptor))
buyProductCallbackCaptor.value.onBuyResult(true)
verify(productView).onBuySuccess()
}
購買失敗的測試(假設Repository.getProduct 購買超過10份即會回傳失敗)
@Test
fun buyFailTest() {
val buyProductCallbackCaptor = argumentCaptor<IProductRepository.BuyProductCallback>()
val productId = "pixel3"
val items = 11
presenter.buy(productId, items)
verify(repository).buy(eq(productId), eq(items), capture(buyProductCallbackCaptor))
buyProductCallbackCaptor.value.onBuyResult(false)
verify(productView).onBuyFail()
}
MVP 架構將程式分為Model、View、Presenter。Presenter從Repository取得資料後,透過View的Interface告訴View要做什麼事。而View就只需要處理現在該呈現什麼資料。把商業邏輯與View的職責分開,這樣一來就讓可測試性提高了。
當我們在測試Presenter,只關注:
1.是否呼叫Repository
2.本身的邏輯是否正確
3.是否呼叫正確的View Callback
如果用Robolectirc來直接測試Activity,只關注。
1.Activity初始化是否有呼叫IPresenter.getProduct。
2.呼叫IProductView.onGetResult,是否有將商品結果放到UI上。
3.呼叫IProductView.onBuySuccess,是否有Toast。
4.呼叫IProductView.onBuyFail,是否有AlertDialog。
範例下載:
https://github.com/evanchen76/MVPUnitTestSample
下一篇將介紹另一個常用的架構MVVM。
出版書:
Android TDD 測試驅動開發:從 UnitTest、TDD 到 DevOps 實踐
線上課程:
Android 動畫入門到進階
Android UI 進階實戰(Material Design Component)