這一篇開始讓TDD在Android實踐。
Google 在Google IO2017 時介紹了怎麼在Android的TDD,
1.先寫Failing UI測試
2.接著依序完成裡面的Feature Dev
3.等所有的Feature Dev完成之後,最後Passing UI Test
4.再進行重構
圖片來源:Google IO 2017
而每個Feature Dev裡會有自已的TDD小循環,裡面的測試就會是單元測試。
圖片來源:Google IO 2017
我們就以先前MVP架構的範例來講MVP的TDD。每個步驟會放在github上的每個commit。
開始之前,先想清楚你要做什麼,MVP有那些interface方法。
步驟:
1.先寫失敗的UI測試
2.寫失敗的Presenter測試
3.實作通過Presenter測試
4.重構Presenter
5.寫失敗的Repository測試
6.實作通過Repository測試
7.重構Repository
8.實作UI通過UI測試
9.重構UI
開始寫測試之前,先把UI拉好。建立好ProductActivity、activity_main.xml。
新增UI測試ProductScreenTest
@RunWith(AndroidJUnit4::class)
@LargeTest
class ProductScreenTest {
@get:Rule
var activityActivityTestRule = ActivityTestRule(ProductActivity::class.java, true, false)
@Test
fun productViewTest() {
val intent = Intent()
activityActivityTestRule.launchActivity(intent)
Thread.sleep(2000)
Espresso.onView(ViewMatchers.withId(R.id.productName))
.check(ViewAssertions.matches(ViewMatchers.withText("Google Pixel 3")))
}
}
執行測試,測試失敗。完成了第一個Failing UI Test。
開始黃色圈圈Feature Dev,也就是單元測試。
圖片來源:Google IO 2017
從Presenter開始測試,我們知道在MVP的架構,Presenter會跟Repository取資料後,呼叫View的Callback更新UI。
@Test
fun getProductTest() {
}
撰寫測試:呼叫presenter.getProduct時是否有呼叫Repository。
class ProductPresenterTest {
private lateinit var presenter: ProductContract.IProductPresenter
@Mock
private lateinit var repository: IProductRepository
@Before
fun setupPresenter() {
MockitoAnnotations.initMocks(this)
presenter = ProductPresenter(repository)
}
@Test
fun getProductTest() {
val productId = "pixel3"
presenter.getProduct(productId)
val loadProductCallbackCaptor = argumentCaptor<IProductRepository.LoadProductCallback>()
verify(repository).getProduct(eq(productId), capture(loadProductCallbackCaptor))
}
}
Production code的ProductPresenter這時候還未實作。
class ProductPresenter(val repository: IProductRepository) : ProductContract.IProductPresenter
{
override fun getProduct(productId: String) {
TODO("not implemented")
}
}
由於這個測試是驗證是否有呼叫IProductRepository.getProduct
,所以這時候的Repository只會有Interface,還不需要實作。
執行測試,完成了第一個失敗的測試。
kotlin.NotImplementedError: An operation is not implemented: not implemented
接著要實作presenter呼叫Repository的部分,讓測試通過。請記得這裡還不需要呼叫view的callback。這個測試還沒有包含view的callback。
class ProductPresenter(val repository: IProductRepository) : ProductContract.IProductPresenter
{
override fun getProduct(productId: String) {
repository.getProduct(productId, object : IProductRepository.LoadProductCallback{
//還沒處理View的callback
})
}
}
測試通過。
接著要測試Presenter.getProduct
完成後是否有Callback View
class ProductPresenterTest {
private lateinit var presenter: ProductContract.IProductPresenter
@Mock
private lateinit var repository: IProductRepository
@Mock
private lateinit var view: ProductContract.IProductView
private var productResponse = ProductResponse()
@Before
fun setupPresenter() {
MockitoAnnotations.initMocks(this)
presenter = ProductPresenter(repository)
productResponse.id = "pixel3"
productResponse.name = "Google Pixel 3"
productResponse.price = 27000
productResponse.desc = "Desc"
}
@Test
fun getProductTest() {
val productId = "pixel3"
presenter.getProduct(productId)
val loadProductCallbackCaptor = argumentCaptor<IProductRepository.LoadProductCallback>()
verify(repository).getProduct(eq(productId), capture(loadProductCallbackCaptor))
loadProductCallbackCaptor.value.onProductResult(productResponse)
verify(view).onGetResult(productResponse)
}
}
執行測試,完成錯誤的測試。
kotlin.NotImplementedError: An operation is not implemented: not implemented
接著完成presenter.getProduct
的ProductionCode
class ProductPresenter(
val repository: IProductRepository,
val view: ProductContract.IProductView
) : ProductContract.IProductPresenter
{
override fun getProduct(productId: String) {
repository.getProduct(productId, object : IProductRepository.LoadProductCallback{
override fun onProductResult(productResponse: ProductResponse) {
view.onGetResult(productResponse)
}
})
}
}
再次執行測試,綠燈。這樣就完成Presenter.getProduct了。
接著開始Repository的功能,陸續的完成各個Feature Dev的測試
新增ProductRepositoryTest,測試是否有呼叫ProductAPI
class ProductRepositoryTest {
lateinit var repository: IProductRepository
@Mock
private lateinit var repositoryCallback: IProductRepository.LoadProductCallback
@Mock
private lateinit var productAPI: IProductAPI
@Before
fun setupPresenter() {
MockitoAnnotations.initMocks(this)
repository = ProductRepository(productAPI)
}
@Test
fun getProductTest() {
repository.getProduct("pixel3", repositoryCallback)
val argumentCaptor = argumentCaptor<IProductAPI.LoadAPICallback>()
verify(productAPI).getProduct(eq("pixel3"), capture(argumentCaptor))
}
}
class ProductRepository(val productAPI: IProductAPI) : IProductRepository {
override fun getProduct(productId: String, capture: IProductRepository.LoadProductCallback) {
TODO("not implemented")
}
}
執行測試,測試失敗。
kotlin.NotImplementedError: An operation is not implemented: not implemented
完成ProductionCode:Repository呼叫ProductAPI
class ProductRepository(val productAPI: IProductAPI) : IProductRepository {
override fun getProduct(productId: String, callback: IProductRepository.LoadProductCallback) {
productAPI.getProduct(productId, object : IProductAPI.LoadAPICallback{
})
}
}
執行測試:綠燈。
接著寫Repository的Callback的測試
@Test
fun getProductTest() {
repository.getProduct("pixel3", repositoryCallback)
val argumentCaptor = argumentCaptor<IProductAPI.LoadAPICallback>()
verify(productAPI).getProduct(eq("pixel3"), capture(argumentCaptor))
//指定ProductAPI回傳值
argumentCaptor.value.onGetResult(productResponse)
//驗證是否有呼叫View
verify(repositoryCallback).onProductResult(productResponse)
}
執行測試:測試失敗。
kotlin.NotImplementedError: An operation is not implemented: not implemented
完成Production code
class ProductRepository(private val productAPI: IProductAPI) : IProductRepository {
override fun getProduct(productId: String, callback: IProductRepository.LoadProductCallback) {
productAPI.getProduct(productId, object : IProductAPI.LoadAPICallback{
override fun onGetResult(productResponse: ProductResponse) {
callback.onProductResult(productResponse)
}
})
}
}
執行測試,綠燈。這樣Repository也完成了,所有的Feature Dev都完成了,接著可以開始想辦法Passing UI Test。
回到Activity,將實作ProductContract.IProductView
,及呼叫productPresenter
class ProductActivity : AppCompatActivity(), ProductContract.IProductView{
private val productId = "pixel3"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val productRepository = ProductRepository(ProductAPI())
val productPresenter = ProductPresenter(productRepository, this)
productPresenter.getProduct(productId)
}
override fun onGetResult(productResponse: ProductResponse) {
productName.text = productResponse.name
productDesc.text = productResponse.desc
val currencyFormat = NumberFormat.getCurrencyInstance()
currencyFormat.maximumFractionDigits = 0
val price = currencyFormat.format(productResponse.price)
productPrice.text = price
}
}
回到UI測試ProductScreenTest,執行UI測試。通過測試。
class ProductScreenTest {
@get:Rule
var activityActivityTestRule = ActivityTestRule(ProductActivity::class.java, true, false)
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getTargetContext()
assertEquals("evan.chen.tutorial.tdd.mvptddsample", appContext.packageName)
}
@Test
fun productViewTest() {
val intent = Intent()
activityActivityTestRule.launchActivity(intent)
Thread.sleep(2000)
Espresso.onView(ViewMatchers.withId(R.id.productName))
.check(ViewAssertions.matches(ViewMatchers.withText("Google Pixel 3")))
}
}
到這裡就到了Passing UI Test了。
接著你可以開始重構。這樣就是一個MVP架構下的TDD。下一篇將介紹MVVM架構下的TDD。
範例下載:
https://github.com/evanchen76/TDDMvpSample