iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 27
1
Mobile Development

Android TDD 測試驅動開發系列 第 27

Day27 - Android MVP 架構下的 TDD

這一篇開始讓TDD在Android實踐。

Google 在Google IO2017 時介紹了怎麼在Android的TDD,

1.先寫Failing UI測試
2.接著依序完成裡面的Feature Dev
3.等所有的Feature Dev完成之後,最後Passing UI Test
4.再進行重構

https://ithelp.ithome.com.tw/upload/images/20191011/20111896K0FrwXTeAg.png
圖片來源:Google IO 2017

而每個Feature Dev裡會有自已的TDD小循環,裡面的測試就會是單元測試。

https://ithelp.ithome.com.tw/upload/images/20191011/20111896Zgt6wn2ctK.png
圖片來源:Google IO 2017

我們就以先前MVP架構的範例來講MVP的TDD。每個步驟會放在github上的每個commit。

https://ithelp.ithome.com.tw/upload/images/20191011/20111896k64uY824pD.png

開始之前,先想清楚你要做什麼,MVP有那些interface方法。
步驟:
1.先寫失敗的UI測試
2.寫失敗的Presenter測試
3.實作通過Presenter測試
4.重構Presenter
5.寫失敗的Repository測試
6.實作通過Repository測試
7.重構Repository
8.實作UI通過UI測試
9.重構UI

Failing UI Test

開始寫測試之前,先把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

開始黃色圈圈Feature Dev,也就是單元測試。
https://ithelp.ithome.com.tw/upload/images/20191011/20111896wcQjEbjQEr.png
圖片來源:Google IO 2017

撰寫Presenter的測試,呼叫Repository

從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

接著開始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


上一篇
Day26 - TDD 測試驅動開發
下一篇
Day28 - Android MVVM 架構下的TDD
系列文
Android TDD 測試驅動開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言