# Unit Test - 使用hilt測試10 - Testing swipe detele Functionality
---
## 開始吧
### 情境
* 透過Espresso模擬輸入資料,寫入Db,模擬swipe delete,驗證資料是否正確刪除
### 建立ShoppingAdapter
```kotlin=
class ShoppingItemAdapter @Inject constructor(
var glide: RequestManager
) : RecyclerView.Adapter<ShoppingItemAdapter.ShoppingItemViewHolder>() {
class ShoppingItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
//效能取捨使用 diffUtil,址更新有變動的項目
private val diffCallBack = object : DiffUtil.ItemCallback<ShoppingItem>() {
override fun areItemsTheSame(oldItem: ShoppingItem, newItem: ShoppingItem): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ShoppingItem, newItem: ShoppingItem): Boolean {
return oldItem.hashCode() == newItem.hashCode()
}
}
private val differ = AsyncListDiffer(this, diffCallBack)
var shoppingItems: List<ShoppingItem>
get() = differ.currentList
set(value) = differ.submitList(value)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShoppingItemViewHolder {
return ShoppingItemViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.item_shopping,
parent,
false
)
)
}
override fun onBindViewHolder(holder: ShoppingItemViewHolder, position: Int) {
val shoppingItem = shoppingItems[position]
holder.itemView.apply {
glide.load(shoppingItem.imageUrl).into(ivShoppingImage)
tvName.text = shoppingItem.name
val amointText = "${shoppingItem.amount}"
tvShoppingItemAmount.text = amointText
val priceText = "${shoppingItem.price}"
tvShoppingItemPrice.text = priceText
}
}
override fun getItemCount(): Int {
return shoppingItems.size
}
}
```
### 撰寫ShoppingFragment功能
* 使用ItemTouchHelper實現滑動刪除
* 設定recycler view
```kotlin=
class ShoppingFragment @Inject constructor(
val shoppingItemAdapter: ShoppingItemAdapter
): Fragment(R.layout.fragment_shopping) {
lateinit var viewModel: ShoppingViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = ViewModelProvider(requireActivity()).get(ShoppingViewModel::class.java)
subscribeToObserver()
setupRecyclerView()
fabAddShoppingItem.setOnClickListener {
findNavController().navigate(
ShoppingFragmentDirections.actionShoppingFragmentToAddShoppingItemFragment())
}
}
//recycler view左右滑動刪除
private val itemTouchCallback = object : ItemTouchHelper.SimpleCallback(
0, LEFT or RIGHT
){
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val pos = viewHolder.layoutPosition
val item = shoppingItemAdapter.shoppingItems[pos]
viewModel.deleteShoppingItem(item) //因為這邊已經先刪除了
//所以這邊如果取消就要把剛剛刪除的資料補回去
Snackbar.make(requireView(), "Successfully deleted item", Snackbar.LENGTH_LONG).apply {
setAction("Undo"){
viewModel.insertShoppingItemIntoDb(item)
}
show()
}
}
}
private fun subscribeToObserver(){
viewModel.shoppingItems.observe(viewLifecycleOwner, Observer {
shoppingItemAdapter.shoppingItems = it
})
viewModel.totalPrice.observe(viewLifecycleOwner, Observer{
val price = it ?: 0f
val priceText = "Total Price $price"
tvShoppingItemPrice.text = priceText
})
}
private fun setupRecyclerView(){
rvShoppingItems.apply {
adapter = shoppingItemAdapter
layoutManager = LinearLayoutManager(requireContext())
ItemTouchHelper(itemTouchCallback).attachToRecyclerView(this)
}
}
}
```
### 修正ShoppingFragment
* 因為在測試時,我們會Lanuch fragment,但viewModel在這邊Create時就註冊了observer,所以要將viewModel當參數傳遞,讓Test也可以參照到這裡的viewModel
```kotlin=
class ShoppingFragment @Inject constructor(
val shoppingItemAdapter: ShoppingItemAdapter,
var viewModel: ShoppingViewModel? = null
): Fragment(R.layout.fragment_shopping) {
// 這行刪除
//lateinit var viewModel: ShoppingViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = viewModel ?: ViewModelProvider(requireActivity()).get(ShoppingViewModel::class.java)
}
```
### 修正TestShoppingFragmentFactory
```kotlin=
class TestShoppingFragmentFactory @Inject constructor(
private val imageAdapter: ImageAdapter,
private val glide: RequestManager,
private val shoppingItemAdapter: ShoppingItemAdapter
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when (className) {
ImagePickFragment::class.java.name -> ImagePickFragment(imageAdapter)
AddShoppingItemFragment::class.java.name -> AddShoppingItemFragment(glide)
//這裡多傳入一個用我們fakeRepo產生的viewModel
ShoppingFragment::class.java.name -> ShoppingFragment(shoppingItemAdapter,
ShoppingViewModel(FakeRepository())
)
else -> super.instantiate(classLoader, className)
}
}
}
```

### 再把剛剛在test新增的內容貼到main project 的FragmentFactory
* 將ShoppingViewModel實例化的參數取消
```kotlin=
class ShoppingFragmentFactory @Inject constructor(
private val imageAdapter: ImageAdapter,
private val glide: RequestManager,
private val shoppingItemAdapter: ShoppingItemAdapter
) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
return when (className) {
ImagePickFragment::class.java.name -> ImagePickFragment(imageAdapter)
AddShoppingItemFragment::class.java.name -> AddShoppingItemFragment(glide)
ShoppingFragment::class.java.name -> ShoppingFragment(shoppingItemAdapter)
else -> super.instantiate(classLoader, className)
}
}
}
```
### 寫測試吧
==幾個重點==
1. 第18行fargment Factory實例化時會帶有viewModel & Adapter參數
2. 第24行,我們launch ShoppingFragment 並attach到我們定義的empty activity,這裡分兩個部分:
1. 測試方的Fragment Factory 會Inject ShoppingItemAdapter,並用fale Repository實例化一個viewModel
2. 在真正app用的Fragment Factory,也會Inject ShoppingItemAdapter,建構子也會帶viewModel,但是viewModel是在ovViewCreated透過viewModelProvider獲取
3. 將testViewModel參照到real ViewModel
4. 透過real viewModel寫入一筆資料
5. 透過Espresso模擬swipe刪除
6. 再驗證testViewModel的item是否有刪除
```kotlin=
@MediumTest
@HiltAndroidTest
@ExperimentalCoroutinesApi
class ShoppingFragmentTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Before
fun setup() {
hiltRule.inject()
}
@Inject
lateinit var testFragmentFactory: TestShoppingFragmentFactory
@Test
fun swipeShoppingItem_deleteItemInDb() {
val shoppingItem = ShoppingItem("Test", 1, 1.2f, "testUrl", 1)
var testViewModel: ShoppingViewModel? = null
launchFragmentInHiltContainer<ShoppingFragment>(
fragmentFactory = testFragmentFactory
) {
testViewModel = viewModel
viewModel?.insertShoppingItemIntoDb(shoppingItem)
}
onView(withId(R.id.rvShoppingItems)).perform(
RecyclerViewActions.actionOnItemAtPosition<ShoppingItemAdapter.ShoppingItemViewHolder>(
0, swipeLeft()
)
)
assertThat(testViewModel?.shoppingItems?.getOrAwaitValue()).isEmpty()
}
@Test
fun clickAddShoppingItemButton_navigateToAddShoppingItemFragment() {
val navController = mock(NavController::class.java)
launchFragmentInHiltContainer<ShoppingFragment> {
Navigation.setViewNavController(requireView(), navController)
}
onView(withId(R.id.fabAddShoppingItem)).perform(click())
verify(navController).navigate(
ShoppingFragmentDirections.actionShoppingFragmentToAddShoppingItemFragment()
)
}
}
```

參考資料
[
Philipp Lackner's channel](https://www.youtube.com/watch?v=V5S2-Mg8B2U)
###### tags: `test` `Unit Test` `hilt` `kotlin`