---
# System prepended metadata

title: Unit Test - 使用hilt測試10 -  Testing swipe detele Functionality
tags: [Unit Test, hilt, test, kotlin]

---

# 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)
        }
    }
}
```

![](https://i.imgur.com/BU6jCpp.png)

### 再把剛剛在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()
        )
    }
}
```
![](https://i.imgur.com/qTORyso.png)

參考資料
[
Philipp Lackner's channel](https://www.youtube.com/watch?v=V5S2-Mg8B2U)

###### tags: `test` `Unit Test` `hilt` `kotlin`