I am trying to unit test the kotlin coroutines. My project is following MVP pattern where the coroutines are used in the presenter like this:
fun authenticateWithUserAndPassword(usernameOrEmail: String, password: String) {
launchUI(strategy) {
view.showLoading()
try {
val token = retryIO("login") {
when {
settings.isLdapAuthenticationEnabled() ->
client.loginWithLdap(usernameOrEmail, password)
usernameOrEmail.isEmail() ->
client.loginWithEmail(usernameOrEmail, password)
else ->
client.login(usernameOrEmail, password)
}
}
val myself = retryIO("me()") { client.me() }
myself.username?.let { username ->
val user = User(
id = myself.id,
roles = myself.roles,
status = myself.status,
name = myself.name,
emails = myself.emails?.map { Email(it.address ?: "", it.verified) },
username = username,
utcOffset = myself.utcOffset
)
localRepository.saveCurrentUser(currentServer, user)
saveCurrentServer.save(currentServer)
localRepository.save(LocalRepository.CURRENT_USERNAME_KEY, username)
saveAccount(username)
saveToken(token)
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
true
)
view.saveSmartLockCredentials(usernameOrEmail, password)
navigator.toChatList()
}
} catch (exception: RocketChatException) {
when (exception) {
is RocketChatTwoFactorException -> {
navigator.toTwoFA(usernameOrEmail, password)
}
else -> {
analyticsManager.logLogin(
AuthenticationEvent.AuthenticationWithUserAndPassword,
false
)
exception.message?.let {
view.showMessage(it)
}.ifNull {
view.showGenericErrorMessage()
}
}
}
} finally {
view.hideLoading()
}
}
}
Here launchUI is defined as
/**
* Launches a coroutine on the UI context.
*
* #param strategy a CancelStrategy for canceling the coroutine job
*/
fun launchUI(strategy: CancelStrategy, block: suspend CoroutineScope.() -> Unit): Job =
MainScope().launch(context = strategy.jobs, block = block)
CancelStrategy class
class CancelStrategy(owner: LifecycleOwner, val jobs: Job) : LifecycleObserver {
init {
owner.lifecycle.addObserver(this)
}
#OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun onDestroy() {
jobs.cancel()
}
}
I am new to coroutine unit testing and tried writing a basic unit test
#Test
fun check_authentication_of_user() = runBlocking {
loginPresenter.authenticateWithUserAndPassword(USERNAME, PASSWORD)
verify(navigator).toChatList()
}
On running the test I am getting the following error.
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method kotlinx.coroutines.BuildersKt__Builders_commonKt.launch, parameter context
Any help regarding how to unit test the given method will be appreciated.
Related
I'm aware that there are a couple of topics on this but none of them solve my issue.
I'm trying to test an implementation of NetworkBoundResource.
inline fun <ResultType, RequestType, ErrorType> networkBoundResource(
crossinline query: () -> Flow<ResultType>,
crossinline fetch: suspend () -> Response<RequestType>,
crossinline saveFetchResult: suspend (RequestType) -> Unit,
crossinline onFetchFailed: (Response<*>?, Throwable?) -> ErrorType? = { _, _ -> null },
crossinline shouldFetch: (ResultType) -> Boolean = { true },
coroutineDispatcher: CoroutineDispatcher
) = flow<Resource<ResultType, ErrorType>> {
val data = query().first()
emit(Resource.Success(data))
if (shouldFetch(data)) {
val fetchResponse = safeApiCall { fetch() }
val fetchBody = fetchResponse.body()
if (fetchBody != null) {
saveFetchResult(fetchBody)
}
if (!fetchResponse.isSuccessful) {
emit(Resource.Error(onFetchFailed(fetchResponse, null)))
} else {
query().map { emit(Resource.Success(it)) }
}
}
}.catch { throwable ->
emit(Resource.Error(onFetchFailed(null, throwable)))
}.flowOn(coroutineDispatcher)
This works as expected in my use case in production code.
override suspend fun getCategories() = networkBoundResource(
query = {
categoryDao.getAllAsFlow().map { categoryMapper.categoryListFromDataObjectList(it) }
},
fetch = {
categoryServices.getCategories()
},
onFetchFailed = { errorResponse, _ ->
categoryMapper.toError(errorResponse)
},
saveFetchResult = { response ->
// Clear the old items and add the new ones
categoryDao.clearAll()
categoryDao.insertAll(categoryMapper.toDataObjectList(response.data))
},
coroutineDispatcher = dispatchProvider.IO
)
I have my test setup like this (using turbine for flow testing).
#OptIn(ExperimentalCoroutinesApi::class)
class NetworkBoundResourceTests {
data class ResultType(val data: String)
sealed class RequestType {
object Default : RequestType()
}
sealed class ErrorType {
object Default : RequestType()
}
private val dispatchProvider = TestDispatchProviderImpl()
#Test
fun `Test`() = runTest {
val resource = networkBoundResource(
query = { flowOf(ResultType(data = "")) },
fetch = { Response.success(RequestType.Default) },
saveFetchResult = { },
onFetchFailed = { _, _ -> ErrorType.Default },
coroutineDispatcher = dispatchProvider.IO
)
resource.test {
}
}
}
The coroutine dispatcher is set to unconfined through DI/Test dispatcher.
I want to test that;
Emitting first data from query, then query is updated and new data from saveFetchResult then query().map { emit(Resource.Success(it)) } emits the updated data from that save result.
I think I need to do something around a spyk on my flow with MockK but I can't seem to figure it out. query() will always return the same flow of data as it's mocked to do so if I awaitItem() again it returns the same data (as it should) as that's what the mock is setup for.
I've found a way to test this. Not exactly how I imagined it in my head.
#Test
fun `Given should fetch is true and fetch throws exception, When retrieving data, Then cached items emitted and error item after`() =
runTest {
val saveFetchResultAction = mockk<(() -> Unit)>("Save results action")
val fetchErrorAction = mockk<(() -> ErrorType)>("Fetch error action")
every { fetchErrorAction() } answers { ErrorType }
val fetchRequestAction = mockk<(() -> Response<RequestType>)>("Fetch request action")
coEvery { fetchRequestAction() } throws (Exception(""))
networkBoundResource(
query = { flowOf(ResultType) },
fetch = { fetchRequestAction() },
saveFetchResult = { saveFetchResultAction() },
onFetchFailed = { _, _ -> fetchErrorAction() },
shouldFetch = { true },
coroutineDispatcher = dispatchProvider.IO
).test {
// Assert that we've got the cached item
val cacheItem = awaitItem()
assertThat(cacheItem).isInstanceOf(Resource.Success::class.java)
val errorItem = awaitItem()
assertThat(errorItem).isInstanceOf(Resource.Error::class.java)
awaitComplete()
// Verify order & calls
verifyOrder {
fetchRequestAction()
fetchErrorAction()
}
verify(exactly = 1) { fetchErrorAction() }
verify(exactly = 1) { fetchRequestAction() }
verify(exactly = 0) { saveFetchResultAction() }
}
}
So I have a ViewModel I'm trying to unit test. It is using the stateIn operator. I found this documentation about how to test stateflows created using the stateIn operator https://developer.android.com/kotlin/flow/test but the mapLatest never triggers even though I'm collecting the flow.
class DeviceConfigurationViewModel(
val systemDetails: SystemDetails,
val step: AddDeviceStep.ConfigureDeviceStep,
val service: DeviceRemoteService
) : ViewModel(), DeviceConfigurationModel {
#OptIn(ExperimentalCoroutinesApi::class)
private val _state: StateFlow<DeviceConfigurationModel.State> =
service.state
.mapLatest { state ->
when (state) {
DeviceRemoteService.State.Connecting -> {
DeviceConfigurationModel.State.Connecting
}
is DeviceRemoteService.State.ConnectedState.Connected -> {
state.sendCommand(step.toCommand(systemDetails))
DeviceConfigurationModel.State.Connected
}
is DeviceRemoteService.State.ConnectedState.CommandSent -> {
DeviceConfigurationModel.State.Configuring
}
is DeviceRemoteService.State.ConnectedState.MessageReceived -> {
transformMessage(state)
}
is DeviceRemoteService.State.Disconnected -> {
transformDisconnected(state)
}
}
}
.distinctUntilChanged()
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000), // Keep it alive for a bit if the app is backgrounded
DeviceConfigurationModel.State.Disconnected
)
override val state: StateFlow<DeviceConfigurationModel.State>
get() = _state
private fun transformDisconnected(
state: DeviceRemoteService.State.Disconnected
): DeviceConfigurationModel.State {
return if (state.hasCause) {
DeviceConfigurationModel.State.UnableToConnect(state)
} else {
state.connect()
DeviceConfigurationModel.State.Connecting
}
}
private fun transformMessage(state: DeviceRemoteService.State.ConnectedState.MessageReceived): DeviceConfigurationModel.State {
return when (val message = state.message) {
is Message.AddedToProject -> DeviceConfigurationModel.State.Configured
is Message.ConfigWifiMessage -> {
if (!message.values.success) {
DeviceConfigurationModel.State.Error(
message.values.errorCode,
state,
step.toCommand(systemDetails)
)
} else {
DeviceConfigurationModel.State.Configuring
}
}
}
}
}
And here's my unit test. The mapLatest never seems to get triggered even though I'm collecting the flow. I'm using the suggestions here https://developer.android.com/kotlin/flow/test
#OptIn(ExperimentalCoroutinesApi::class)
class DeviceConfigurationViewModelTest {
private val disconnectedService = mock<DisconnectedService>()
private val deviceServiceState: MutableStateFlow<DeviceRemoteService.State> =
MutableStateFlow(DeviceRemoteService.State.Disconnected(disconnectedService, Exception()))
private val deviceService = mock<DeviceRemoteService> {
on { state } doReturn deviceServiceState
}
private val systemDetails = mock<SystemDetails> {
on { controllerAddress } doReturn "192.168.1.112"
on { controllerName } doReturn "000FFF962FE7"
}
private val step = AddDeviceDeviceStep.ConfigureDeviceStep(
44,
"Thou Shalt Not Covet Thy Neighbor’s Wifi",
"testing616"
)
private lateinit var viewModel: DeviceConfigurationViewModel
#Before
fun setup() {
viewModel = DeviceConfigurationViewModel(systemDetails, step, deviceService)
}
#Test
fun testDeviceServiceDisconnectWithCauseMapsToUnableToConnect() =
runTest {
val collectJob = launch(UnconfinedTestDispatcher()) { viewModel.state.collect() }
deviceServiceState.emit(
DeviceRemoteService.State.Disconnected(Exception("Something bad happened"))
)
assertThat(viewModel.state.value).isInstanceOf(DeviceConfigurationModel.State.UnableToConnect::class.java)
collectJob.cancel()
}
}
I believe this is happening because the viewModelScope uses a hardcoded Main dispatcher under the hood.
You can follow the instructions here in the Android documentation to see how you can to set the Main dispatcher for tests.
I'm trying to create this MockController with mockk to avoid create a new class for testing.
Is possible to do that?
class MockController : IController {
override lateinit var output: (String) -> Unit
override fun start() {
output("OK")
}
}
Class to test:
class ClassToTest(
private val controller: IController,
private val output: (String) -> Unit
){
fun start() {
controller.output = { result ->
output(result)
}
controller.start()
}
}
Then I use like this TEST example:
#Test
fun checkOutputIsCalled() {
runBlocking {
var outputCalled = false
val outputClassToTest: (String) -> Unit = {
outputCalled = true
}
val classToTest = ClassToTest(MockController(), outputClassToTest)
classToTest.start()
delay(1000)
assert(outputCalled)
}
}
I'm trying to update:
#Test
fun checkOutputIsCalled() {
runBlocking {
val controller = spyk<IController>()
var outputCalled = false
val outputClassToTest: (String) -> Unit = {
outputCalled = true
}
val classToTest = ClassToTest(controller, outputClassToTest)
every { controller.start() } answers {
controller.output.invoke("OK")
} //When I execute the test, output is null because yet doesn't exist the output creted inside ClassToTest
classToTest.start()
delay(1000)
assert(outputCalled)
}
}
When I execute the test, output is null because yet doesn't exist the output creted inside ClassToTest
How this could be after the output assign?
Thanks!
You should mock your output object and your Controller. Once done, tell your mocked controller to return the mocked output when property is called. Right after the start() invocation you can verify that output lambda was invoked. Please note that all your mocks must be relaxed.
class ClassToTestTesting {
companion object {
const val INVOCATION_PARAM = "OK"
}
#Test
fun test() = runBlocking {
val paramOutput: (String) -> Unit = mockk(relaxed = true)
val controller: IController = mockk(relaxed = true) {
every { output } returns paramOutput
every { start() } answers { output.invoke(INVOCATION_PARAM) }
}
val classToTest = ClassToTest(
controller,
paramOutput
)
classToTest.start()
verify { paramOutput(INVOCATION_PARAM) }
}
}
I have a function that return flow by emitAll
fun handle(actions: MoviesActions): Flow<MoviesStates> = flow {
when (actions) {
is MoviesActions.LoadMovies -> {
emit(MoviesStates.Loading)
emitAll(moviesUseCase.execute())
}
}
}
And this the use case function
suspend fun execute(): Flow<MoviesStates> = flow {
combine(f1, f2) { state1: MoviesStates, state2: MoviesStates ->
// some code
}.collect {
emit(it)
}
}
No problem in testing the first emission MoviesStates.Loading, the problem is when I try to test the flow which return from usecase by emitAll emitAll(moviesUseCase.execute()), the test fails and I got this result
java.util.NoSuchElementException: Expected at least one element
this is my unit test
#Test
fun testLoadMovies() = runBlocking {
whenever(useCase.execute()).thenReturn(flow {
MoviesStates.EmptyList
})
val actual = viewModel.handle(MoviesActions.LoadMovies).drop(1).first()
val expected = MoviesStates.EmptyList
assertEquals(actual, expected)
}
So How can I test it correctly?
Thanks to gpunto , this is the solution he suggested
#Test
fun testLoadMovies() = runTest {
whenever(useCase.execute()).thenReturn(flow {
MoviesStates.EmptyList
})
useCase.execute().collectLatest { states ->
val actual = viewModel.handle(MoviesActions.LoadMovies).drop(1).first()
val expected = states
assertEquals(expected, actual)
}
}
I know that there are a lot of posts "How to cancel Coroutines Scope" but I couldn't find the answer for my case.
I have an Array of objects that I want to send each of them to Server using Coroutines.
What I need is, if one of my requests returns error, canceling others.
Here is my code:
private fun sendDataToServer(function: () -> Unit) {
LiabilitiesWizardSessionManager.getLiabilityAddedDocuments().let { documents ->
if (documents.isEmpty().not()) {
CoroutineScope(Dispatchers.IO).launch {
documents.mapIndexed { index, docDetail ->
async {
val result = uploadFiles(docDetail)
}
}.map {
var result = it.await()
}
}
} else function.invoke()
}
}
Below is my uploadFiles() function:
private suspend fun uploadFiles(docDetail: DocDetail): ArchiveFileResponse? {
LiabilitiesWizardSessionManager.mCreateLiabilityModel.let { model ->
val file = File(docDetail.fullFilePath)
val crmCode = docDetail.docTypeCode
val desc = docDetail.docTypeDesc
val id = model.commitmentMember?.id
val idType = 1
val createArchiveFileModel = CreateArchiveFileModel(108, desc, id, idType).apply {
this.isLiability = true
this.adaSystem = 3
}
val result = mRepositoryControllerKotlin.uploadFile(file, createArchiveFileModel)
return when (result) {
is ResultWrapper.Success -> {
result.value
}
is ResultWrapper.GenericError -> {
null
}
is ResultWrapper.NetworkError -> {
null
}
}
}
}
I know, I'm missing something.