Product listing and detail – Part 2

In this part we are going to see how to write test cases for product listing and product detail screen. Check out Product listing and detail screen blog to know about how we can integrate retrofit to fetch data from api and display in a list and use android jetpack navigation component to display the detail screen.

Let’s start writing test cases from the model layer.

First we need to write test cases for the ProductRepository.kt

As we already know that we should not use actual repository for testing as it will make the the api. Let’s create a fake repository for product listing first.

FakeProductRepository.kt

class FakeProductRepository : ProductRepositoryInterface {
  private val productList = arrayListOf<ProductModel>().apply {
      add(
          ProductModel(
              1,
              "Title 1",
              "Description 1",
              "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
          )
      )
      add(
          ProductModel(
              2,
              "Title 2",
              "Description 2",
              "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
          )
      )
      add(
          ProductModel(
              3,
              "Title 3",
              "Description 3",
              "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
          )
      )
      add(
          ProductModel(
              4,
              "Title 4",
              "Description 4",
              "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
          )
      )
      add(
          ProductModel(
              5,
              "Title 5",
              "Description 5",
              "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
          )
      )
      add(
          ProductModel(
              6,
              "Title 6",
              "Description 6",
              "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
          )
      )
  }

  override suspend fun getTaskList(): ApiResult<ArrayList<ProductModel>> {
      return ApiResult.Success(productList)
  }
}

Next create a test class ProductRepositoryTest.kt file to write test cases for product repository.

class ProductRepositoryTest {
    private val productRepository = FakeProductRepository()

    @Test
    fun `product is fetched`() = runBlocking {
        val result = productRepository.getProductList()
        if (result is ApiResult.Success) {
            Assert.assertTrue(result.data?.isNotEmpty() ?: false)
        }
    }
}

Since we are having only one function to fetch the product list we can test that function whether it returns the list or not.

Next let’s write some unit test cases for ProductListViewModel. 

The ProductListViewModel needs a repository to initialize. We need to pass the FakeProductRepository in the constructor.

ProductListViewModelTest.kt

class ProductListViewModelTest {
  private lateinit var productListViewModel: ProductListViewModel

  @get:Rule
  val instantTaskExecutorRule = InstantTaskExecutorRule()

  @Before
  fun setUp() {
      productListViewModel = ProductListViewModel(FakeProductRepository())
  }
  @Test
   fun fetchProductList() {
     val data = productListViewModel.productListMLD.getOrAwaitValue()
     assertTrue(data != null)
   }
}

The above test cases will check if product list are fetched or not.

Next we need to test fragments and activity.

Each fragment and activity got their own viewmodel with injected repositories. While writing an instrumented test we need to keep in mind that we need to inject fake repositories to the viewModels. 

If you are not familiar with integrating hilt for testing check out our Login Testing video to know in detail. LoginTesting

Inside the TestModule.kt file tell hilt how to provide the ProductRepositoryInterface instance for testing.

TestModule.kt

@TestInstallIn(components = [SingletonComponent::class], replaces = [AppModule::class])
@Module
object TestModule {

  @Singleton
  @Provides
  fun loginRepository(): LoginRepositoryInterface {
      return FakeLoginRepository()
  }

  @Singleton
  @Provides
  fun productRepository(): ProductRepositoryInterface {
      return FakeProductRepository()
  }
}

Next step,

Add this dependency to use the navigation controller.

// Testing Navigation
androidTestImplementation "androidx.navigation:navigation-testing:$nav_version"

Next step, let’s write instrumented test cases for product detail fragment view.

ProductDetailFragmentTest.kt

Test cases :

  1. Check if product detail screen is displayed
  2. Check if product detail is displayed with the given product.
@HiltAndroidTest
class ProductDetailFragmentTest : BaseHiltTest() {
  private val navController =
      TestNavHostController(ApplicationProvider.getApplicationContext())

  override fun startUp() {
      super.startUp()

      val bundle = Bundle()
      bundle.putParcelable("productDetail", ProductModel(1,"title", "desc", "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"))

      launchFragmentInHiltContainer<ProductDetailFragment>(bundle) {
          navController.setGraph(R.navigation.product_nav_graph)
          navController.setCurrentDestination(R.id.productDetailFragment, bundle)
          Navigation.setViewNavController(requireView(), navController)
      }
  }

  @Test
  fun checkIsDetailViewDisplayed() {
      ViewMatchers.assertThat(
          navController.currentDestination?.id,
          CoreMatchers.equalTo(R.id.productDetailFragment)
      )
  }

  @Test
  fun checkCorrectProductDataIsDisplayed() {
      Espresso.onView(ViewMatchers.withId(R.id.title_tv))
          .check(ViewAssertions.matches(ViewMatchers.withText("title")))

      Espresso.onView(ViewMatchers.withId(R.id.description_tv))
          .check(ViewAssertions.matches(ViewMatchers.withText("desc")))
  }
}

Next step, we need to check the product listing screen. 

  1. We need to verify that the product listing screen is displayed or not.
  2. On selecting the product, the product detail screen should be displayed.
@HiltAndroidTest
class ProductListFragmentTest : BaseHiltTest() {

  private val navController = TestNavHostController(ApplicationProvider.getApplicationContext())

  override fun startUp() {
      super.startUp()
      launchFragmentInHiltContainer<ProductListFragment> {
          navController.setGraph(R.navigation.product_nav_graph)
          Navigation.setViewNavController(requireView(), navController)
      }
  }

  @Test
  fun whenOpened_ProductListShouldBeDisplayed() {
      assertThat(navController.currentDestination?.id, equalTo(R.id.productListFragment))
  }

  @Test
  fun whenProductSelected_detailShouldBeDisplayed() {

      onView(withId(R.id.product_list_rv)).perform(
          RecyclerViewActions.actionOnItemAtPosition<ProductListAdapter.ProductItemViewHolder>(
              1,
              ViewActions.click()
          )
      )

      assertThat(navController.currentDestination?.id, equalTo(R.id.productDetailFragment))
  }
}

Next step we need to test the Product activity.

To start an activity we need to add the activity scenero first, It will start the activity.

Then we need to check

  1. product list is displayed or not
  2. On selecting product it should navigate to the product detail page.
  3. On pressing back button it should navigate back to the list screen.
@HiltAndroidTest
class ProductActivityTest : BaseHiltTest() {

  @get:Rule(order = 1)
  val activityScenario = ActivityScenarioRule(ProductActivity::class.java)

  private lateinit var navController: NavController

  override fun startUp() {
      super.startUp()
      Intents.init()
      activityScenario.scenario.onActivity {
          navController = it.findNavController(R.id.nav_host_fragment)
      }
  }

  override fun cleanUp() {
      super.cleanUp()
      Intents.release()
  }

  @Test
  fun checkProductListIsDisplayed() {

      ViewMatchers.assertThat(
          navController.currentDestination?.id,
          CoreMatchers.equalTo(R.id.productListFragment)
      )
  }

  @Test
  fun whenProductSelected_productDetailShouldBeDisplayed() {

      Espresso.onView(ViewMatchers.withId(R.id.product_list_rv)).perform(
          RecyclerViewActions.actionOnItemAtPosition<ProductListAdapter.ProductItemViewHolder>(
              1,
              ViewActions.click()
          )
      )

      ViewMatchers.assertThat(
          navController.currentDestination?.id,
          CoreMatchers.equalTo(R.id.productDetailFragment)
      )
  }

  @Test
  fun whenUserPressBackFromDetail_productListScreenShouldBeDisplayed() {

      whenProductSelected_productDetailShouldBeDisplayed()

      Espresso.pressBack()

      checkProductListIsDisplayed()
  }
}

That’s it we have completed the writing test cases for product listing and detail feature.

Hope this blog helps you to understand how we can test activities, fragments and navigation controllers and how to use hilt dependency injection in testing.

Happy learning
Team Appmetry

Leave a Reply