2
votes

I have the following classes

interface CarsApi {
    suspend fun fetchCar() : Car
}

class FetchCarUseCase(private val carsApi: CarsApi) {
    suspend fun execute: Car = withContext(dispatcherProvider.io()) {
        carsApi.fetchCar()
    }
}

class ViewModel(private val fetchCarUseCase: FetchCarUseCase) {

     private var car: Car

     suspend fun retrieveCar() {
        car = fetchCarUseCase.execute()
     }
}

I want to write an ermetic test for the viewModel and the useCase:

@Test
fun testCarFetching() = runBlockingTest {
  val aCar = Car()
  val mockApi = mock<CarsApi>()
  `when`(mockApi.fetchCar()).thenReturn(aCar)
  val fetchCarUseCase = FetchCarUseCase(mockApi)
  val viewModel = ViewModel(fetchCarUseCase)

  viewModel.retrieveCar()

  /* assert stuff on viewModel.car*/
}

But the viewModel.car always seems to be null. Inside the test body mockApi.fetchCar() does retrieve the provided value, but inside the FetchCarUseCase it does not. Also if I remove the suspend keyword from the interface, the mocking seems to be working fine.

At the moment, due to some other conditions I cannot use Mockk library, so I'm stuck with Mockito.

Am I missing something?

The used dependencies: testImplementation 'junit:junit:4.12' testImplementation 'org.mockito:mockito-core:2.28.2' testImplementation('com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0') { exclude module: 'mockito-core' } testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.2

1
Were u able to solve this? - iori24

1 Answers

2
votes

In case anyone else has to deal with this problem, here is the infrastructure I have build.

First, in all the classes that launch threads inject through the constructor or property a kotlinx.coroutines.DispatcherProvider. In my case it was just the useCase, but the viewModel might require it, as well.

    class FetchCarUseCase(private val dispatcher: CoroutineDispatcher,
                          private val carsApi: CarsApi) {
    suspend fun execute: Car = withContext(dispatcher) {
        carsApi.fetchCar()
    }
}

In the unit tests project, add a helper rule-class, in order to extract some functionality:

 @ExperimentalCoroutinesApi
class CoroutineTestRule(val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()) : TestWatcher() {

    val testDispatcherProvider = object : DispatcherProvider {
        override fun default(): CoroutineDispatcher = testDispatcher
        override fun io(): CoroutineDispatcher = testDispatcher
        override fun main(): CoroutineDispatcher = testDispatcher
        override fun unconfined(): CoroutineDispatcher = testDispatcher
    }

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
        testDispatcher.cleanupTestCoroutines()
    }
}

And finally the unit test looks like this:

    @ExperimentalCoroutinesApi
    @RunWith(MockitoJUnitRunner::class)
    class ViewModelTest {
        @get:Rule
        var coroutinesTestRule = CoroutineTestRule()

        @Test
    fun testCarFetching() = coroutinesTestRule.testDispatcher.runBlockingTest {
      val aCar = Car()
      val mockApi = mock<CarsApi>()
      `when`(mockApi.fetchCar()).thenReturn(aCar)
      val fetchCarUseCase = FetchCarUseCase(mockApi)
      val viewModel = ViewModel(fetchCarUseCase)

      viewModel.retrieveCar()

      /* assert stuff on viewModel.car*/
    }

   @Test
    fun testCarFetchingError() = coroutinesTestRule.testDispatcher.runBlockingTest {
      val aCar = Car()
      val mockApi = mock<CarsApi>()
      `when`(mockApi.fetchCar()).then {
            throw Exception()
        }

      val fetchCarUseCase = FetchCarUseCase(mockApi)
      val viewModel = ViewModel(fetchCarUseCase)

      viewModel.retrieveCar()

      /* assert stuff on erros*/
    }
}

This way all the code in the unit tests runs on the same thread and in the same context.