Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save anthonymonori/d8b8e32e2b36c59ab44e364fc7d6ddf3 to your computer and use it in GitHub Desktop.
Save anthonymonori/d8b8e32e2b36c59ab44e364fc7d6ddf3 to your computer and use it in GitHub Desktop.
Example workaround for suspend operator overloading in Kotlin when using MockK testing library

The problem

Let's consider the following code:

class UseCase() {
    suspend operator fun invoke(param1: String) = Unit
}
class MyViewModel @Inject constructor(val useCase: UseCase) {
    fun init() {
        viewModelScope.launch {
            useCase("inputParam")
        }
    }
}
class MyViewModelTest() {
    @MockK
    private lateinit var useCase: UseCase
    
    private lateinit var viewModel: MyViewModel
    
    @BeforeTest
    private setUp() {
        MockKAnnotations.init(this)
        viewModel = MyViewModel(useCase)
    }
    
    @Test
    fun `Test case 1`() {
        viewModel.init()
        coVerify { useCase("inputParam") }
        confirmVerified(useCase)
    }
    
    @Test
    fun `Test case 2 where I need to override the useCase result`() {
        coEvery { useCase(any()) } throws Exception("Unexpected error")
        try {
            viewModel.init()
        } catch(e: Exception) {
            assertEquals("Unexpected error", e.message)
        }
    }
}

The error you would get:

io.mockk.MockKException: no answer found for: UseCase(#1).invoke("inputParam", continuation {})

Read more around the issue here: mockk/mockk#288

Workaround

The workaround is two part:

  1. Most cases where you only need one mock to return the same output, the solution is to simply move it to the @BeforeTest setup phase of your unit test, where this issue doesn't come up
  2. If you need to test different outputs, you would need to call coVerify again, so to avoid this problem, you have to reinitialize your class with this new mock and clear the mock beforehand
class MyViewModelTest() {
    @MockK
    private lateinit var useCase: UseCase
    
    private lateinit var viewModel: MyViewModel
    
    @BeforeTest
    private setUp() {
        MockKAnnotations.init(this)
        coEvery { useCase(any()) } coAnswers { Unit } // Moving it to BeforeTest does help alleviate the issue 
        viewModel = MyViewModel(useCase)
    }
    
    @Test
    fun `Test case 1`() {
        viewModel.init()
        coVerify { useCase("inputParam") }
        confirmVerified(useCase)
    }
    
    @Test
    fun `Test case 2 where I need to override the useCase result`() {
        clearMocks(useCase) // Clear mock here
        coEvery { useCase(any()) } throws Exception("Unexpected error")
        viewModel = MyViewModel(useCase) // need to reinstantiate
        try {
            viewModel.init()
        } catch(e: Exception) {
            assertEquals("Unexpected error", e.message)
        }
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment