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)
}
}
Related
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'm seeing some odd behavior. I have a simple StateFlow<Boolean> in my ViewModel that is not being collected in the fragment. Definition:
private val _primaryButtonClicked = MutableStateFlow(false)
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked
and here is where I set the value:
fun primaryButtonClick() {
_primaryButtonClicked.value = true
}
Here is where I'm collecting it.
repeatOnOwnerLifecycle {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
repeatOnOwnerLifecycle:
inline fun Fragment.repeatOnOwnerLifecycle(
state: Lifecycle.State = Lifecycle.State.RESUMED,
crossinline block: suspend CoroutineScope.() -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(state) {
block()
}
}
What am I doing wrong? The collector never fires.
Does this make sense?
val primaryButtonClicked: StateFlow<Boolean> = _primaryButtonClicked.asStateFlow()
Also I couldn't understand the inline function part, because under the hood seems you wrote something like this
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
launch(dispatchProvider.io()) {
freeSimPurchaseFragmentViewModel.primaryButtonClicked.collect {
if (it) {
autoCompletePlacesStateFlowModel.validateErrors()
formValidated = autoCompletePlacesStateFlowModel.validateAddress()
if (formValidated) {
freeSimPurchaseFragmentViewModel
.sumbitForm(autoCompletePlacesStateFlowModel.getStateFlowCopy())
}
}
}
}
}
}
Why are you launching one coroutine in another and collect the flow from IO dispatcher? You need to collect the values from the main dispatcher.
My project has a lot of operations that must be performed one after another. I was using listeners, but I found this tutorial Kotlin coroutines on Android and I wanted to change my sever call with better readable code. But I think I am missing something. The below code always return an error from getTime1() function:
suspend fun getTimeFromServer1() :ResultServer<Long> {
val userId = SharedPrefsHelper.getClientId()
return withContext(Dispatchers.IO) {
val call: Call<ResponseFromServer>? = userId?.let { apiInterface.getTime(it) }
(call?.execute()?.body())?.run {
val time:Long? = this.data?.time
time?.let {
Timber.tag("xxx").e("time received it ${it}")// I am getting the right result here
ResultServer.Success(it)
}
Timber.tag("xxx").e("time received ${time}")
}
ResultServer.Error(Exception("Cannot get time"))
}
}
fun getTime1() {
GlobalScope.launch {
when (val expr: ResultServer<Long> = NetworkLayer.getTimeFromServer1()) {
is ResultServer.Success<Long> -> Timber.tag("xxx").e("time is ${expr.data}")
is ResultServer.Error -> Timber.tag("xxx").e("time Error") //I am always get here
}}
}
}
But if I am using listeners (getTime()) everything works perfectly:
suspend fun getTimeFromServer(savingFinishedListener: SavingFinishedListener<Long>) {
val userId = SharedPrefsHelper.getClientId()
withContext(Dispatchers.IO) {
val call: Call<ResponseFromServer>? = userId?.let { apiInterface.getTime(it) }
(call?.execute()?.body())?.run {
val time:Long? = this.data?.time
time?.let {
Timber.tag("xxx").e("time received it ${it}")
savingFinishedListener.onSuccess(it)
}
}
savingFinishedListener.onSuccess(null)
}
}
fun getTime() {
GlobalScope.launch {
NetworkLayer.getTimeFromServer(object:SavingFinishedListener<Long>{
override fun onSuccess(t: Long?) {
t?.let {
Timber.tag("xxx").e("time here $it") //I am getting the right result
}
}
})
}
}
Thanks in advance for any help.
The last line of a lambda is implicitly the return value of that lambda. Since you don't have any explicit return statements in your withContext lambda, its last line:
ResultServer.Error(Exception("Cannot get time"))
means that it always returns this Error. You can put return#withContext right before your ResultServer.Success(it) to make that line of code also return from the lambda.
Side note: don't use GlobalScope.
Hi I have a rxjava flat map in which I want to call a coroutine usecase onStandUseCase which is an api call
Initially the use case was also rxjava based and it used to return Observable<GenericResponse> and it worked fine
now that I changed the use to be coroutines based it only returns GenericResponse
how can modify the flatmap to work fine with coroutines use case please
subscriptions += view.startFuellingObservable
.onBackpressureLatest()
.doOnNext { view.showLoader(false) }
.flatMap {
if (!hasOpenInopIncidents()) {
//THIS IS WHERE THE ERROR IS IT RETURNS GENERICRESPONSE
onStandUseCase(OnStandUseCase.Params("1", "2", TimestampedAction("1", "2", DateTime.now()))) {
}
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
Observable.just(incidentOpenResponse)
}
}
.subscribe(
{ handleStartFuellingClicked(view, it) },
{ onStartFuellingError(view) }
)
OnStandUseCase.kt
class OnStandUseCase #Inject constructor(
private val orderRepository: OrderRepository,
private val serviceOrderTypeProvider: ServiceOrderTypeProvider
) : UseCaseCoroutine<GenericResponse, OnStandUseCase.Params>() {
override suspend fun run(params: Params) = orderRepository.notifyOnStand(
serviceOrderTypeProvider.apiPathFor(params.serviceType),
params.id,
params.action
)
data class Params(val serviceType: String, val id: String, val action: TimestampedAction)
}
UseCaseCoroutine
abstract class UseCaseCoroutine<out Type, in Params> where Type : Any {
abstract suspend fun run(params: Params): Type
operator fun invoke(params: Params, onResult: (type: Type) -> Unit = {}) {
val job = GlobalScope.async(Dispatchers.IO) { run(params) }
GlobalScope.launch(Dispatchers.Main) { onResult(job.await()) }
}
}
startFuellingObservable is
val startFuellingObservable: Observable<Void>
Here is the image of the error
Any suggestion on how to fix this please
thanks in advance
R
There is the integration library linking RxJava and Kotlin coroutines.
rxSingle can be used to turn a suspend function into a Single. OP wants an Observable, so we can call toObservable() for the conversion.
.flatMap {
if (!hasOpenInopIncidents()) {
rxSingle {
callYourSuspendFunction()
}.toObservable()
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
Observable.just(incidentOpenResponse)
}
}
Note that the Observables in both branches contain just one element. We can make this fact more obvious by using Observable#concatMapSingle.
.concatMapSingle {
if (!hasOpenInopIncidents()) {
rxSingle { callYourSuspendFunction() }
} else {
val incidentOpenResponse = GenericResponse(false)
incidentOpenResponse.error = OPEN_INCIDENTS
Single.just(incidentOpenResponse)
}
}