ViewModel Testing with JUnit5, MockK & Turbine

ViewModel Testing with JUnit5, MockK & Turbine

Introduction:

Unit testing your viewmodel class is one of the most important steps in your Android application development as it is the only place where you manage all your UiState and interact with the data layer, so you need to ensure everything works as expected in this layer.

JUnit5 is the next generation of JUnit, which is the famous library for testing on JVM.

MockK is a mocking library for Kotlin that allows you to create mock/fake objects in your Android unit tests and instrumented tests so that you can then use those objects to define expected outcomes in your tests.

Turbine is a small testing library for Kotlin Flows.

Dependency:

testImplementation "org.junit.jupiter:junit-jupiter:5.9.3"
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
testImplementation "io.mockk:mockk:1.13.5"
testImplementation "io.mockk:mockk-android:1.13.5"
testImplementation 'app.cash.turbine:turbine:1.0.0'

Test environment setup:

We have to test our ViewModel function calls, I am expecting that by default you are using viewModelScopeto create your coroutine calls.

While using viewmodelscope.launch, by default, is hardcoded to use the Dispatchers.Main CoroutineDispatcher. But, in our test environment, we can not make calls on this, so we need to set a fallback whenever we have a Main dispatcher call, we have to shift that to our TestDispatcher.

We can use Dispatchers.setMain to achieve this.

So, beforeEach test we will set the TestDispatcher and afterEach test we will reset it.

But, this thing will be common for all of our ViewModel test classes, so instead of doing this in every test class, we will create a base test class that every test class will override.

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach

@OptIn(ExperimentalCoroutinesApi::class)
open class BaseTest {

    @BeforeEach
    open fun beforeEach() {
        Dispatchers.setMain(UnconfinedTestDispatcher())
    }

    @AfterEach
    open fun afterEach() {
        Dispatchers.resetMain()
    }
}

Note: You can achieve the same behavior by using the MainDispatcherRule, but for some unknown(comment iyk) it was only working for JUnit4 tests, so use my way if you’re using JUnit5.

Now our test class will look something like this.

class FeedbackViewModelTest : BaseTest() {

    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
    }

    @AfterEach
    override fun afterEach() {
        super.afterEach()
    }
}

Implementation:

  1. In your viewmodel, remember to inject only the classes that you can easily mock(manually write the behaviour).
private val repo: FeedbackRepoImpl = mockk(relaxed = true)

2. We need to initialize the viewmodel instance with all the mocked dependencies.(in my case only a repo).

class FeedbackViewModelTest : BaseTest() {

    private lateinit var viewModel: FeedbackViewModel

    private val repo: FeedbackRepoImpl = mockk(relaxed = true)


    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
        viewModel = spyk(
            FeedbackViewModel(
                repo
            )
        )
    }
    ...
    ...
}

3. Don’t forget to clear the mocks afterEach test.

@AfterEach
override fun afterEach() {
    super.afterEach()
    clearAllMocks()
}

4. I hope you’re following the best practices for viewmodel and coroutines.

So, you’re exposing a StateFlow<UiState<*>> object from your viewmodel and getting a ResponseState<*> from your data layer.

5. Write your first test:

    @Test
    fun `loadFeedbackQuestions with valid fid, return success`() = runTest {

    }

Understanding the behaviour..

Thing to test: When we have the success state from our data layer, we expect to have a UiState.Success in our StateFlow.

Required: We need to have the success response state after making the call from our mocked repo object

5.1 We will use the mockK for faking the repo function call .

coEvery { repo.getFeedbackQuestions(any(), any()) } returns ResponseState.Success(FeedbackQuesDTO())

You can mock the behavior with every/coEvery functions from mockK.

5.2 Now, we know when we make the call, we will have the success from data layer. So make it..

viewModel.loadFeedbackQuestions(fid)

5.3 Now, also verify that the call was made using the coVerify fun from mockK.

coVerify { repo.getFeedbackQuestions(any(), fid) }

5.4 Now, we have made the call and we know we also have the success response from our mocked repo object. So just assert the viewmodel Unit where we should assert to have the UiState.Success.

viewModel.feedbackQuestions.test {
    assert(awaitItem() is UiState.Success)
}

P.s. In my case feedbackQuestions was a StateFlow object that has to be updated in the viewModel.loadFeedbackQuestions(fid) call.

Our Test file will look:

class FeedbackViewModelTest : BaseTest() {

    private lateinit var viewModel: FeedbackViewModel

    private val repo: FeedbackRepoImpl = mockk(relaxed = true)


    @BeforeEach
    override fun beforeEach() {
        super.beforeEach()
        viewModel = spyk(
            FeedbackViewModel(
                repo,
                ""
            )
        )
    }

    @AfterEach
    override fun afterEach() {
        super.afterEach()
        clearAllMocks()
    }

    @Test
    fun `loadFeedbackQuestions with valid fid, return success`() = runTest {
        val fid = "valid"
        coEvery { repo.getFeedbackQuestions(any(), any()) } returns ResponseState.Success(
            FeedbackQuesDTO()
        )
        viewModel.loadFeedbackQuestions(fid)
        coVerify { repo.getFeedbackQuestions(any(), fid) }
        viewModel.feedbackQuestions.test {
            assert(awaitItem() is UiState.Success)
        }
    }
  }

For me it worked perfectly fine with Junit5, mockK and turbine. If you faced any issues then drop a comment below.

I hope you found this helpful. If yes, then do FOLLOW ‘Sagar Malhotra’ for more Android-related content.

#androidWithSagar #android #androiddevelopment #development #compose #kotlin #proandroiddev #google #androiddev