mockk, how to use slot for MutableMap<String, String> - unit-testing

using mockk 1.9.3
having a function to be verified
class EventLogger private constructor()
fun logUserEvent(eventName: String?, eventParamMap: MutableMap<String, String>?) {
......
internaLogEventImpl(eventName, eventParamMap)
}
internal fun internaLogEventImpl(eventName: String?, customParams: MutableMap<String, String>?) {
......
}
companion object {
#Volatile
private var sEventLoggerSingleton: EventLogger? = null
#JvmStatic
val instance: EventLogger
get() {
if (sEventLoggerSingleton == null) {
sEventLoggerSingleton = EventLogger()
}
return sEventLoggerSingleton!!
}
}
got compiler error at every {eventLogger.internaLogEventImpl(any(), mapSlot)}
Type mismatch.
Required: MutableMap<String, String>?
Found: CapturingSlot<MutableMap<String, String>>
when trying this below :
class TestK {
lateinit var eventLogger: EventLogger
lateinit var application: Application
val mapSlot = slot<MutableMap<String, String>>()
#Before
fun setUp() {
application = ApplicationProvider.getApplicationContext<Application>()
eventLogger = mockk.spyk(EventLogger.instance)
ReflectionHelpers.setStaticField(EventLogger::class.java, "sEventLoggerSingleton", eventLogger)
}
#After
fun cleanUp() {
ReflectionHelpers.setStaticField(EventLogger::class.java, "sEventLoggerSingleton", null)
}
#Test
fun logNotificationStatusChange_with_enabled_WhenCalled_ShouldLog() {
val testMap = hashMapOf("action" to "open")
every {eventLogger.internaLogEventImpl(any(), mapSlot)} answers {
println(mapSlot.captured)
assert(mapSlot.captured["action"] == "open")
}
eventLogger.logUserEvent("test_event", testMap)
}
}

You need to use capture (see Mockk's Capturing section).
So for your case capture(mapSlot) should work.
eventLogger.internaLogEventImpl(any(), capture(mapSlot))
Before trying to mock on complex code, It would be better to learn on easier example.
Here is a working example to mock a private call on an object with mockk.
object MyLogger {
fun logUserEvent(event: String?, map: MutableMap<String, String>?) {
// turns the event string into an uppercase string.
internaLogEventImpl(event?.toUpperCase(), map)
}
private fun internaLogEventImpl(event: String?, map: MutableMap<String, String>?): Unit =
throw Exception("real implementation")
}
How to test and mock the internal function so we don't throw the exception.
#Test
fun `test logger internal`() {
val expectedMap = mutableMapOf("a" to "b")
val expectedEvent = "EVENT"
val mock = spyk(MyLogger, recordPrivateCalls = true)
justRun { mock["internaLogEventImpl"](expectedEvent, expectedMap) }
// or justRun { mock["internaLogEventImpl"](any<String>(), any<MutableMap<String, String>>()) }
mock.logUserEvent("event", expectedMap)
verify { mock["internaLogEventImpl"](expectedEvent, expectedMap) }
}
Here logUserEvent calls the real implementation and internaLogEventImpl calls the mock implementation.
if justRun { mock["internaLogEventImpl"](expectedEvent, expectedMap) } is not called (or wrong because the argument doesn't match), the real implementation will be call. Here it will throw Exception("real implementation").
Please try and modify values to check different behaviors.

Related

unit test coroutine nullpointerexception when mocking

I am unit testing the following class
class LoadTrendingSearchUseCaseImp #Inject constructor(
private val searchCriteriaProvider: SearchCriteriaProvider,
private val coroutineDispatcherProvider: CoroutineDispatcherProvider
) : LoadTrendingSearchUseCase {
override suspend fun execute(): List<String> {
return withContext(coroutineDispatcherProvider.io()) {
searchCriteriaProvider.provideTrendingSearch().trendingSearches
}
}
}
interface SearchCriteriaProvider {
suspend fun provideTrendingSearch(): CatalogSearchPage
}
class SearchCriteriaProviderImp() : SearchCritieraProvider {
override suspend fun provideTrendingSearch(): CatalogSearchPage {
return withContext(coroutineDispatcherProvider.io()) {
/* long running task */
}
}
}
interface CoroutineDispatcherProvider {
fun io(): CoroutineDispatcher = Dispatchers.IO
fun default(): CoroutineDispatcher = Dispatchers.Default
fun main(): CoroutineDispatcher = Dispatchers.Main
fun immediate(): CoroutineDispatcher = Dispatchers.Main.immediate
fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
}
class CoroutineDispatcherProviderImp #Inject constructor() : CoroutineDispatcherProvider
This is my actual test:
class LoadTrendingSearchUseCaseImpTest {
private val searchCriteriaProvider: SearchCriteriaProvider = mock()
private val coroutineDispatcherProvider = CoroutineDispatcherProviderImp()
private lateinit var loadTrendingSearchUseCaseImp: LoadTrendingSearchUseCaseImp
#Before
fun setUp() {
loadTrendingSearchUseCaseImp = LoadTrendingSearchUseCaseImp(
searchCriteriaProvider,
coroutineDispatcherProvider
)
}
#Test
fun `should provide trending searches`() {
runBlockingTest {
// Arrange
// EXCEPTION HERE whenever(searchCriteriaProvider.provideTrendingSearch().trendingSearches).thenReturn(
emptyList()
)
// Act
val actualResult = loadTrendingSearchUseCaseImp.execute()
// Assert
assertThat(actualResult).isEmpty()
}
}
}
The actual error message:
java.lang.NullPointerException
.product_search.usecase.imp.LoadTrendingSearchUseCaseImpTest$should provide trending searches$1.invokeSuspend(LoadTrendingSearchUseCaseImpTest.kt:30)
You tried to chain invocations when stubbing a method.
whenever(searchCriteriaProvider.provideTrendingSearch().trendingSearches)
.thenReturn(emptyList())
During stubbing, the actual methods are being called.
searchCriteriaProvider.provideTrendingSearch() returns null, as this call is not stubbed yet
subsequent call null.trendingSearches results in NPE
You need to stub each call in the chain
whenever(searchCriteriaProvider.provideTrendingSearch())
.thenReturn(catalogSearchPage)
whenever(catalogSearchPage.trendingSearches)
.thenReturn(emptyList())
Obviously, this assumes that
catalogSearchPage is also a mock
trendingSearches is a property
Alternatively, you can construct a POJO for catalogSearchPage, and return it in the first stubbing.

Unit testing and high-order functions in Kotlin

So i have a base class:
abstract class BaseService<M : MyMetric>(private val otherService: OtherService) {
abstract fun <T> monitor(metric: M, action: () -> T): T
protected fun <T> monitorDefault(metric: M, action: () -> T): T {
val begin = System.currentTimeMillis()
return try {
notifyEvent(metric.total)
val result = action()
notifyEvent(metric.success)
result
} catch (e: CommonException) {
//handling
}
}
and simple implementation:
class PrintMetricService(otherService: OtherService): BaseService<PrintMetric>(otherService) {
override fun <T> monitor(metric: PrintMetric, action: () -> T) : T {
return monitorDefault(metric, action)
}
}
then i use this method:
#Service
class PrintReportService(private val reportService: ReportService,
private val metricService: MetricService<PrintMetric>) {
fun getReport(): String {
return metricService.monitor(PrintMetric) {
val response = try {
reportService.renderPdf(jsonRequest, TEMPLATE_CODE, channel)
} catch (e: Exception) {
throw CommonException(PRINT_SERVICE_ERROR_EXCEPTION_CODE)
}
response.body.data!!
}
}
now I want to write a unit test for getReport() (we use TestNg and mockito in project):
class PrintReportServiceImplTest {
#InjectMocks
private lateinit var printReportService: PrintReportServiceImpl
#Mock
private lateinit var reportService: ReportService
#Mock
private lateinit var metricService: PrintMetricService
#BeforeMethod
fun setUp() {
openMocks(this)
whenever(reportService.renderPdf().thenReturn("response (base64)")
}
#Test
fun getReportTest() {
val report = printReportService.getReport()
assertNotNull(report)
}
}
the test crashes with an error: java.lang.AssertionError: expected object to not be null
But if i add inline when override monitor(metric: M, action: () -> T) in PrintMetricService the test will pass. What could be the problem? Is this a feature of the high-order functions in Kotlin?

Unable to mock a method - Mockito - Kotlin

I am trying to mock a method (getQualityControlCheckDataForUplift(ArgumentMatchers.any(), ArgumentMatchers.anyFloat())) to return qcchecks which I have defined in my test case but I get an error.
Any suggestions on where I am doing it wrong please
org.mockito.exceptions.base.MockitoException: Unable to create mock instance of type 'QualityControlChecksDataProvider'
QualityControlChecksDataProvider
class QualityControlChecksDataProvider #Inject constructor(
private val offlineDataStorage: OfflineDataStorage,
private val app: App
) {
private val mapOfQCChecksForMilestone = LinkedHashMap<String, ArrayList<QualityControlCheck>?>()
fun getQualityControlCheckDataForUplift(qualityControlMilestone: QualityControlMilestone, uplift: Float):
ArrayList<QualityControlCheck>? {
val qcChecksForUplift: ArrayList<QualityControlCheck>? = ArrayList()
val qcChecksForMilestone = mapOfQCChecksForMilestone[qualityControlMilestone.milestoneText]
qcChecksForMilestone?.forEach {
if (it.uplift == uplift) qcChecksForUplift?.add(it)
}
return qcChecksForUplift
}
}
In my unit test this is how I am trying to mock
#Test
fun `upliftedVolumeUpdated abcd` () {
val qualityControlCheckDataProvider = spy(QualityControlChecksDataProvider::class.java)
every(qualityControlCheckDataProvider.getQualityControlCheckDataForUplift(ArgumentMatchers.any(), ArgumentMatchers.anyFloat())).thenReturn(qualityControlChecks)
}
I don't think a spy is what you want here I think you want a mock. Give this a try
#Test
fun `upliftedVolumeUpdated abcd` () {
val qualityControlDataProviderMock = mock<QualityControlChecksDataProvider> {
on { getQualityControlCheckDataForUplift(any(), any()) } doReturn qualityControlChecks
}
}

mock retrofit suspend function infinite response

I would like to test case when server does not return response, and we trigger the next network call ( like for example search query).
So we basically have a method inside ViewModel and Retrofit method
interface RetrofitApi {
#GET("Some Url")
suspend fun getVeryImportantStuff(): String
}
class TestViewModel(private val api: RetrofitApi) : ViewModel() {
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val response = api.getVeryImportantStuff()
//DO SOMETHING WITH RESPONSE
}
}
}
And I would like to test case when new query is asked, and the old one didn't returns.
for case when response returns test is easy
#Test
fun testReturnResponse() {
runBlockingTest {
//given
val mockApi:RetrofitApi = mock()
val viewModel = TestViewModel(mockApi)
val response = "response from api"
val query = "fancy query"
whenever(mockApi.getVeryImportantStuff()).thenReturn(response)
//when
viewModel.load(query)
//then
//verify what happens
}
}
But I don't know how to mock suspend function that did't come back, and test case when new request is triggered like this
#Test
fun test2Loads() {
runBlockingTest {
//given
val mockApi:RetrofitApi = mock()
val viewModel = TestViewModel(mockApi)
val response = "response from api"
val secondResponse = "response from api2"
val query = "fancy query"
whenever(mockApi.getVeryImportantStuff())
.thenReturn(/* Here return some fancy stuff that is suspend* or something like onBlocking{} stub but not blocking but dalayed forever/)
.thenReturn(secondResponse)
//when
viewModel.load(query)
viewModel.load(query)
//then
//verify that first response did not happens , and only second one triggered all the stuff
}
}
Any ideas ?
EDIT: I'm not really attached to mockito, any mock library will be good :)
regards
Wojtek
I came up with kind of solution to the problem, but slightly different than I was thinking at the beginning
interface CoroutineUtils {
val io: CoroutineContext
}
interface RetrofitApi {
#GET("Some Url")
suspend fun getVeryImportantStuff(query: String): String
}
class TestViewModel(private val api: RetrofitApi,
private val utils: CoroutineUtils) : ViewModel() {
private val text = MutableLiveData<String>()
val testStream: LiveData<String> = text
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val response = withContext(utils.io) { api.getVeryImportantStuff(query) }
text.postValue(response)
}
}
}
And the test scenario would look like this
class TestViewModelTest {
#get:Rule
val coroutineScope = MainCoroutineScopeRule()
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var retrofit: RetrofitApi
lateinit var utils: CoroutineUtils
val tottalyDifferentDispatcher = TestCoroutineDispatcher()
lateinit var viewModel: TestViewModel
#Before
fun setup() {
retrofit = mock()
utils = mock()
viewModel = TestViewModel(retrofit, utils)
}
#UseExperimental(ExperimentalCoroutinesApi::class)
#Test
fun test2Loads() {
runBlockingTest {
//given
val response = "response from api"
val response2 = "response from api2"
val query = "fancy query"
val query2 = "fancy query2"
whenever(utils.io)
.thenReturn(tottalyDifferentDispatcher)
val mutableListOfStrings = mutableListOf<String>()
whenever(retrofit.getVeryImportantStuff(query)).thenReturn(response)
whenever(retrofit.getVeryImportantStuff(query2)).thenReturn(response2)
//when
viewModel.testStream.observeForever {
mutableListOfStrings.add(it)
}
tottalyDifferentDispatcher.pauseDispatcher()
viewModel.load(query)
viewModel.load(query2)
tottalyDifferentDispatcher.resumeDispatcher()
//then
mutableListOfStrings shouldHaveSize 1
mutableListOfStrings[0] shouldBe response2
verify(retrofit, times(1)).getVeryImportantStuff(query2)
}
}
}
It is not exactly what I wanted, because retrofit call is not triggered when load method is called for the first time, but it is the closest solution.
What would be a perfect test for me will be assertion that retrofit was called twice , but only the second one returned to ViewModel. Solution for that will be to wrap Retrofit around method that returns suspend function like this
interface RetrofitWrapper {
suspend fun getVeryImportantStuff(): suspend (String)->String
}
class TestViewModel(private val api: RetrofitWrapper,
private val utils: CoroutineUtils) : ViewModel() {
private val text = MutableLiveData<String>()
val testStream: LiveData<String> = text
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val veryImportantStuff = api.getVeryImportantStuff()
val response = withContext(utils.io) {
veryImportantStuff(query)
}
text.postValue(response)
}
}
}
and test for it
#Test
fun test2Loads() {
runBlockingTest {
//given
val response = "response from api"
val response2 = "response from api2"
val query = "fancy query"
val query2 = "fancy query2"
whenever(utils.io)
.thenReturn(tottalyDifferentDispatcher)
val mutableListOfStrings = mutableListOf<String>()
whenever(retrofit.getVeryImportantStuff())
.thenReturn(suspendCoroutine {
it.resume { response }
})
whenever(retrofit.getVeryImportantStuff()).thenReturn(suspendCoroutine {
it.resume { response2 }
})
//when
viewModel.testStream.observeForever {
mutableListOfStrings.add(it)
}
tottalyDifferentDispatcher.pauseDispatcher()
viewModel.load(query)
viewModel.load(query2)
tottalyDifferentDispatcher.resumeDispatcher()
//then
mutableListOfStrings shouldHaveSize 1
mutableListOfStrings[0] shouldBe response2
verify(retrofit, times(2)).getVeryImportantStuff()
}
}
But in my opinion it is a little bit too much in interference in code only to be testable. But maybe I'm wrong :P
Looks like you want to test scenario when you have unreachable server, timeout or something similar.
In this case while doing your mock you can say that on first try it returns object and then on second executions throws exception like java.net.ConnectException: Connection timed out.
whenever(mockApi.getVeryImportantStuff())
.thenReturn(someObjet)
.thenThrow(ConnectException("timed out"))
And this this should work but you will have to do try/catch block in ViewModel witch is not ideal. I would suggest you to add additional abstraction.
You could you Repository or UseCase or whatever pattern/name you like to move the network call there. Then introduce sealed class Result to encapsulate behaviour and make your ViewModel more readable.
class TestViewModel(val repo: Repo): ViewModel() {
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
when (repo.getStuff()) {
is Result.Success -> TODO()
is Result.Failure -> TODO()
}
}
}
}
class Repo(private val api: Api) {
suspend fun getStuff() : Result {
return try {
Result.Success(api.getVeryImportantStuff())
} catch (e: java.lang.Exception) {
Result.Failure(e)
}
}
}
sealed class Result {
data class Success<out T: Any>(val data: T) : Result()
data class Failure(val error: Throwable) : Result()
}
interface Api {
suspend fun getVeryImportantStuff() : String
}
With that level of abstraction your ViewModelTest only checks what happens in two cases.
Hope that's helpful!

Unit test with Coroutines and Retrofit

I created an App using coroutines and retrofit and it works fine. The problem comes when I try to create UT for the Presenter. Here how I made the presenter:
class MainPresenter : ViewModel() {
private val compositeDisposable = CompositeDisposable()
private val heroesRepository: HeroesRepository = heroesRepositoryModel.instance()
private lateinit var listener: ActivityStatesListener
fun setActivityListener(listener: ActivityStatesListener) {
this.listener = listener
}
fun getHeroesFromRepository(page: Int) {
GlobalScope.launch(Dispatchers.Main) {
try {
val response = heroesRepository.getHeroes(page)
listener.onHeroesReady(response.data.results)
} catch (e: HttpException) {
listener.onError(e.message())
} catch (e: Throwable) {
listener.onError(e.message)
}
}
}
override fun onCleared() {
super.onCleared()
compositeDisposable.dispose()
}
}
I started to make the UT for it and I made a small test but is giving me the following error: java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize
class HeroesDataSourceTest {
val heroesRepository: HeroesRepository = mock(HeroesRepository::class.java)
#Mock
lateinit var activityListener: ActivityStatesListener
val hero = Heroes.Hero(1, "superman", "holasuperman", 1, null, null)
val results = Arrays.asList(hero)
val data = Heroes.Data(results)
val dataResult = Heroes.DataResult(data)
private val mainPresenter = MainPresenter()
#Before
fun initTest() {
MockitoAnnotations.initMocks(this)
}
#Test
fun testLoadInitialSuccess() = runBlocking(Dispatchers.Main) {
`when`(heroesRepository.getHeroes(0)).thenReturn(dataResult)
mainPresenter.getHeroesFromRepository(0)
verify(activityListener).onHeroesReady(dataResult.data.results)
}
}
Is clear that Dispatcher.Main is giving problems but I have no clue how to solve it.
EDIT
The repository used is the following:
class HeroesRepository {
val privateKey = "5009bb73066f50f127907511e70f691cd3f2bb2c"
val publicKey = "51ef4d355f513641b490a80d32503852"
val apiDataSource = DataModule.create()
val pageSize = 20
suspend fun getHeroes(page: Int): Heroes.DataResult {
val now = Date().time.toString()
val hash = generateHash(now + privateKey + publicKey)
val offset: Int = page * pageSize
return apiDataSource.getHeroes(now, publicKey, hash, offset, pageSize).await()
}
fun generateHash(variable: String): String {
val md = MessageDigest.getInstance("MD5")
val digested = md.digest(variable.toByteArray())
return digested.joinToString("") {
String.format("%02x", it)
}
}
}
I assume that heroesRepository.getHeroes(page) marked as suspend, so it will suspend the coroutine and not block the Main thread.
Try to follow the next approach:
// add `CoroutineContext` to the constructor to be replaceable from the tests
class MainPresenter(private val uiContext: CoroutineContext = Dispatchers.Main)
: ViewModel(), CoroutineScope {
private var job: Job = Job()
override val coroutineContext: CoroutineContext
get() = uiContext + job
fun getHeroesFromRepository(page: Int) {
// use local scope to launch a coroutine
launch {
try {
val response = heroesRepository.getHeroes(page)
listener.onHeroesReady(response.data.results)
} catch (e: HttpException) {
listener.onError(e.message())
} catch (e: Throwable) {
listener.onError(e.message)
}
}
}
override fun onCleared() {
super.onCleared()
job.cancel()
}
// ...
}
In the test class replace uiContext with another CoroutineContext:
class HeroesDataSourceTest {
// ... initializations
#Test
fun testLoadInitialSuccess() = runBlocking {
`when`(heroesRepository.getHeroes(0)).thenReturn(dataResult)
mainPresenter = MainPresenter(Dispatchers.Unconfined).apply {
getHeroesFromRepository(0)
}
// ... your tests here
}
}