Unit Test Với Những Cách Làm TỐT Hơn
Bài đăng này đã không được cập nhật trong 4 năm
Việc triển khai Unit Test vào project Android đang dần phổ biến hơn và anh em chúng ta đang muốn kiểm tra lại chất lượng phần logic trước khi muốn phát hành production. Vậy thì có cách nào TỐT cho công việc hiệu quả hơn không? Chúng ta sẽ cùng tìm hiểu nhé!
(Hình ảnh Kim tự tháp trong kiểm thử) bạn thấy Unit Test là công việc trước tiên nằm ở phần đáy và sau cùng là UI test. Theo thống kê mức độ quan trọng của 3 hình thức test này, người ta chỉ ra rằng :
- UI Tests: 10%
- Integration Tests: 20%
- Unit Tests: 70%
Mình chuẩn bị một ứng dụng nhỏ (hình ảnh trên) có chức năng tìm kiếm đồ ăn với 1 vài tính năng chính:
- Tìm kiếm
- Đồ ăn ưa thích
- Xem chi tiết
Khi chúng ta có cái gì đó để xem trước thì việc hình dung mình nghĩ sẽ đơn giản hơn nhiều, vì vậy đây là demo giúp tiếp cận việc triển khai Unit Test dễ dàng hơn. Các bạn xem tiếp cấu trúc project này như nào nhé. (hình ảnh dưới)
Ok, bây giờ chúng ta sẽ tìm cách triển khai Unit Test cho project này làm sao để TỐT hơn.
Bước 1 Kiểm Tra Mô Hình Project Bạn Đang Dùng
Hiện tại mình đang không theo mô hình nào cả, viết là cứ ném vào activity để xử lý logic thôi.
SearchResultsActivity.kt
fun Context.searchResultsIntent(query: String): Intent {
return Intent(this, SearchResultsActivity::class.java).apply {
putExtra(EXTRA_QUERY, query)
}
}
private const val EXTRA_QUERY = "EXTRA_QUERY"
class SearchResultsActivity : ChildActivity() {
private val repository: RecipeRepository by lazy {RecipeRepository.getRepository(this)}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_list)
val query = intent.getStringExtra(EXTRA_QUERY)
supportActionBar?.subtitle = query
search(query)
retry.setOnClickListener { search(query) }
}
private fun search(query: String) {
showLoadingView()
repository.getRecipes(query, object : RecipeRepository.RepositoryCallback<List<Recipe>> {
override fun onSuccess(recipes: List<Recipe>?) {
if (recipes != null && recipes.isNotEmpty()) {
showRecipes(recipes)
} else {
showEmptyRecipes()
}
}
override fun onError() {
showErrorView()
}
})
}
private fun showEmptyRecipes() {
loadingContainer.visibility = View.GONE
errorContainer.visibility = View.GONE
list.visibility = View.VISIBLE
noresultsContainer.visibility = View.VISIBLE
}
private fun showRecipes(recipes: List<Recipe>) {
loadingContainer.visibility = View.GONE
errorContainer.visibility = View.GONE
list.visibility = View.VISIBLE
noresultsContainer.visibility = View.GONE
list.layoutManager = LinearLayoutManager(this)
list.adapter = RecipeAdapter(recipes, object : RecipeAdapter.Listener {
override fun onClickItem(item: Recipe) {
startActivity(recipeIntent(item.sourceUrl))
}
override fun onAddFavorite(item: Recipe) {
item.isFavorited = true
repository.addFavorite(item)
list.adapter.notifyItemChanged(recipes.indexOf(item))
}
override fun onRemoveFavorite(item: Recipe) {
repository.removeFavorite(item)
item.isFavorited = false
list.adapter.notifyItemChanged(recipes.indexOf(item))
}
})
}
private fun showErrorView() {
loadingContainer.visibility = View.GONE
errorContainer.visibility = View.VISIBLE
list.visibility = View.GONE
noresultsContainer.visibility = View.GONE
}
private fun showLoadingView() {
loadingContainer.visibility = View.VISIBLE
errorContainer.visibility = View.GONE
list.visibility = View.GONE
noresultsContainer.visibility = View.GONE
}
}
Các bạn nhìn vào function search, đảm nhận luôn cả phần logic và hiển thị thay đổi view.
private fun search(query: String) {
showLoadingView()
repository.getRecipes(query, object : RecipeRepository.RepositoryCallback<List<Recipe>> {
override fun onSuccess(recipes: List<Recipe>?) {
if (recipes != null && recipes.isNotEmpty()) {
showRecipes(recipes)
} else {
showEmptyRecipes()
}
}
override fun onError() {
showErrorView()
}
})
}
Đây là Khó khăn chung mà nhiều anh em sẽ gặp phải nếu như không có sự lựa chọn mô hình tối ưu ngay từ đầu. Dẫn đến việc bây giờ muốn triển khai Unit Test NHANH và TỐT tốn nhiều mồ hôi công sức.
Giải pháp: Bạn thử thay đổi sang MVP hoặc MVVM xem sao. (Nếu bạn chưa biết có thể tự tìm hiểu nha)
Bước 2 Refactor Code Theo MVP
Mình sẽ áp dụng cấu trúc MVP để cho việc implement Unit Test trở lên dễ dàng hơn.
Giờ mình sẽ chuyển toàn bộ logic phần search vào 1 presenter có tên SearchPresenter và khi viết unit test thì chỉ kiểm tra function login của presenter này mà thôi. Let's go!
SearchPresenter.kt
class SearchPresenter {
// 1
private var view: View? = null
// 2
fun attachView(view: View) {
this.view = view
}
// 3
fun detachView() {
this.view = null
}
// 4
fun search(query: String) {
// 5
if (query.trim().isBlank()) {
view?.showQueryRequiredMessage()
} else {
view?.showSearchResults(query)
}
}
// 6
interface View {
fun showQueryRequiredMessage()
fun showSearchResults(query: String)
}
}
Tiếp theo integrate presenter này vào view (chính là activity search)
SearchActivity.kt
class SearchActivity : ChildActivity(), SearchPresenter.View {
private val presenter: SearchPresenter = SearchPresenter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
// 2
presenter.attachView(this)
searchButton.setOnClickListener {
val query = ingredients.text.toString()
// 3
presenter.search(query)
}
}
override fun onDestroy() {
// 4
presenter.detachView()
super.onDestroy()
}
// 5
override fun showQueryRequiredMessage() {
// Hide keyboard
val view = this.currentFocus
if (view != null) {
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
Snackbar.make(searchButton, getString(R.string.search_query_required), Snackbar
.LENGTH_LONG).show()
}
// 6
override fun showSearchResults(query: String) {
startActivity(searchResultsIntent(query))
}
}
Lần lượt các bạn tạo thêm các presenter khác :
SearchResultsPresenter gắn cho SearchResultsActivity
Nếu bạn lần đầu dùng mô hình này thì đừng quá lo dần dần sẽ quen thôi. Giờ mình chuyển sang phần viết Unit Test cho những presenter mà anh em vừa mới refactor xong nhé.
Bước 3 Triển khai Unit Test dùng Mockito
Implement cho Kotlin:
dependencies {
...
testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
...
}
Nếu dùng Java:
testImplementation 'junit:junit:4.13'
testImplementation 'org.mockito:mockito-core:2.27.0'
testImplementation "org.powermock:powermock-module-junit4:2.0.4"
testImplementation "org.powermock:powermock-api-mockito2:2.0.4"
- Viết SearchPresenterTest
SearchPresenterTest
class SearchPresenterTest {
private lateinit var presenter : SearchPresenter
private lateinit var view : SearchPresenter.View
@Before
fun setup() {
presenter = SearchPresenter()
view = mock()
presenter.attachView(view)
}
Thêm method test khi query nội dung trống
@Test
fun search_withEmptyQuery_callsShowQueryRequiredMessage() {
presenter.search("")
verify(view).showQueryRequiredMessage()
}
Trường hợp khi đã có sẵn kết quả trong lần search trước đó, chúng ta sẽ gọi hàm showRecipes để hiển thị kết quả.
@Test
fun search_withRepositoryHavingRecipes_callsShowRecipes() {
// 1
val recipe = Recipe("id", "title", "imageUrl", "sourceUrl", false)
val recipes = listOf<Recipe>(recipe)
// 2
doAnswer {
val callback: RepositoryCallback<List<Recipe>> = it.getArgument(1)
callback.onSuccess(recipes)
}.whenever(repository).getRecipes(eq("eggs"), any())
// 3
presenter.search("eggs")
// 4
verify(view).showRecipes(eq(recipes))
}
Tiếp đến xem method addFavorite có chạy đúng không?
@Test
fun addFavorite_shouldUpdateRecipeStatus() {
// 1
val recipe = Recipe("id", "title", "imageUrl", "sourceUrl", false)
// 2
presenter.addFavorite(recipe)
// 3
Assert.assertTrue(recipe.isFavorited)
}
Không thể thiếu trường hợp chúng ta gọi api search thành công
@Test
fun search_callsGetRecipes() {
presenter.search("eggs")
verify(repository).getRecipes(eq("eggs"), any())
}
Tổng kết
Với những chia sẻ trên đây hi vọng rằng các bạn đã có cách triển khai Unit Test cho project của mình, mong rằng bài viết mang lại hữu ích. Việc triển khai Unit Test đạt hiệu quả cao bạn cần chú ý đến việc sử dụng mô hình cho thích hợp và lường trước được nhiều case càng tốt. Nếu có thắc mắc các bạn hãy để lại comment ngay phía dưới bài viết này nhé.
All rights reserved