I'm using coroutines for the first time and I'm having troubles testing my work on a ViewModel.
The test fails with
Wanted but not invoked:
observer.onChanged(<Capturing argument>);
The test is the following:
val androidVersioningMock: Versioning.AndroidVersioning = mock {
on { required } doAnswer { "3.3.6" }
}
val versioningMock: Versioning = mock {
on { android } doAnswer { androidVersioningMock }
}
whenever(networkManager.getAppVersionAsync()).thenReturn(GlobalScope.async { versioningMock })
runBlocking {
updateVersionModel =
UpdateViewModel(application, coroutineDispatcherProvider).apply {
updateLiveData.observeForever(stateObserver)
}
verify(stateObserver).onChanged(stateCaptor.capture())
assertTrue(stateCaptor.lastValue is UpdateState.NoUpdate)
assertEquals(UpdateState.NoUpdate, stateCaptor.lastValue)
}
I have mocked the coroutineDispatcherProvider with
#ExperimentalCoroutinesApi
override val coroutineDispatcherProvider = mock<CoroutineDispatcherProvider> {
on { main } doAnswer { TestCoroutineContext() }
on { io } doAnswer { TestCoroutineContext() }
}
And in my ViewModel, the method that fails is
private suspend fun getUpdateVersion(): Versioning =
withContext(coroutineDispatcherProvider.io) {
networkManager.getAppVersionAsync().await()
}
which is executed like :
launch {
val versioningModel = getUpdateVersion()
...
}
Am I not mocking something or not doing something? Thanks in advance!
The TestCoroutineContext dispatcher is useful to handle timing within tests, but you want to run asynchronous calls synchronously. You should be able to achieve this with the Unconfined dispatcher.
I believe better approach is to pass dispatcher to viewModels, so tests are super easy..
see this https://stackoverflow.com/a/62629410/2719243
Related
Fragment
private fun makeApiRequest() {
vm.getRandomPicture()
var pictureElement = vm.setRandomPicture()
GlobalScope.launch(Dispatchers.Main) {
// what about internet
if (pictureElement != null && pictureElement!!.fileSizeBytes!! < 400000) {
Glide.with(requireContext()).load(pictureElement!!.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
getRandomPicture()
}
}
}
viewmodel
fun getRandomPicture() {
viewModelScope.launch {
getRandomPictureItemUseCase.build(Unit).collect {
pictureElement.value = it
Log.d("inspirationquotes", "VIEWMODEL $pictureElement")
Log.d("inspirationquotes", "VIEWMODEL VALUE ${pictureElement.value}")
}
}
}
fun setRandomPicture(): InspirationQuotesDetailsResponse? {
return pictureElement.value
}
Flow UseCase
class GetRandomPictureItemUseCase #Inject constructor(private val api: InspirationQuotesApi): BaseFlowUseCase<Unit, InspirationQuotesDetailsResponse>() {
override fun create(params: Unit): Flow<InspirationQuotesDetailsResponse> {
return flow{
emit(api.getRandomPicture())
}
}
}
My flow task from viewmodel doesn't goes on time. I do not know how to achieve smooth downloading data from Api and provide it further.
I was reading I could use runBlocking, but it is not recommended in production as well.
What do you use in your professional applications to achieve nice app?
Now the effect is that that image doesn't load or I have null error beacause of my Log.d before GlobalScope in Fragment (it is not in code right now).
One more thing is definding null object I do not like it, what do you think?
var pictureElement = MutableStateFlow<InspirationQuotesDetailsResponse?>(null)
EDIT:
Viewmodel
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
fragment
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { response ->
if (response.fileSizeBytes < 600000) {
Log.d("fragment", "itGetsValue")
Glide.with(requireContext()).load(response.url)
.into(layout.ivRandomPicture)
layout.ivRandomPicture.visibility = View.VISIBLE
} else {
onFloatingActionClick()
}
}
}
Edit2 problem on production, another topic:
Link -> What is the substitute for runBlocking Coroutines in fragments and activities?
First of all, don't use GlobalScope to launch a coroutine, it is highly discouraged and prone to bugs. Use provided lifecycleScope in Fragment:
lifecycleScope.launch {...}
Use MutableSharedFlow instead of MutableStateFlow, MutableSharedFlow doesn't require initial value, and you can get rid of nullable generic type:
val pictureElement = MutableSharedFlow<InspirationQuotesDetailsResponse>()
But I guess we can get rid of it.
Method create() in GetRandomPictureItemUseCase returns a Flow that emits only one value, does it really need to be Flow, or it can be just a simple suspend function?
Assuming we stick to Flow in GetRandomPictureItemUseCase class, ViewModel can look something like the following:
val randomPicture: Flow<InspirationQuotesDetailsResponse> = getRandomPictureItemUseCase.build(Unit)
And in the Fragment:
private fun makeApiRequest() = lifecycleScope.launch {
vm.randomPicture
.flowWithLifecycle(lifecycle, State.STARTED)
.collect { response ->
// .. use response
}
}
Dependency to use lifecycleScope:
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
Apologies for what is probably a very amateur question.
I'm getting to grips with flows and having issues with testing where MutableSharedFlow is concerned.
The following is the simplest example I can construct that recreates the problem:
#ExperimentalCoroutinesApi
#ExperimentalTime
class MyExampleTest {
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
#Test
fun test() = testDispatcher.runBlockingTest {
val sharedFlow = MutableSharedFlow<String>()
sharedFlow.take(2).collect {
println(it)
}
sharedFlow.tryEmit("Hello")
sharedFlow.tryEmit("World")
}
}
This results int he following error:
java.lang.IllegalStateException: This job has not completed yet
at kotlinx.coroutines.JobSupport.getCompletionExceptionOrNull(JobSupport.kt:1187)
at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:53)
at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:80)
at com.example.MyExampleTest.test(MyExampleTest.kt:22)
From my limited understanding I think it's something to do with the fact that SharedFlow never completes. But I thought having the take(2) would mitigate this. Any suggestions would be appreciated!
From my limited understanding I think it's something to do with the fact that SharedFlow never completes.
You're right, this is more or less the problem. Flow is based on Coroutines and the coroutine has not completed.
The best way to unit test Flows in my opinion is Turbine:
https://github.com/cashapp/turbine
// Cold Flow
flowOf("one", "two").test {
assertEquals("one", expectItem())
assertEquals("two", expectItem())
expectComplete()
}
// Hot Flow
MutableStateFlow("test").test {
assertThat(expectItem()).isEqualTo("test")
cancelAndConsumeRemainingEvents()
}
There is also an open issue about this exact "problem":
https://github.com/Kotlin/kotlinx.coroutines/issues/1204
Inside tests use runBlocking instead of runBlockingTest.
For flow testing combine it with a launch from scope. E.g:
val testDispatcher = TestCoroutineDispatcher()
val testScope = TestCoroutineScope(testDispatcher)
runBlocking {
val job1 = testScope.launch {
doJobThatNeedsCollect(Unit).collect {
when (it) {
is Success -> {
isSuccess = true
cancel()
}
is Failure -> {
isSuccess = false
cancel()
}
}
}
}
while (!job1.isCancelled) {
}
assertTrue(isSuccess)
}
I have a method that makes an API call and if an error occurs it will retry the call with a different instance of the same service API.
var getResponse = myApi?.getCodeApi()
if (getResponse?.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
// Retrying with instance of service with a different token
getResponse = newMyApiService?.getCodeApi()
}
checkResponse(getResponse)
What is the right way to unit test the above code?. I tried something like this and it does not seem to work.
whenever(myAPi.getCodeApi()).thenReturn(properResponse)
val errorResponse : Response<DataModel> = mock()
whenever(response.code()).thenReturn(HttpsURLConnection.HTTP_UNAUTHORIZED)
whenever(myAPi.getCodeApi()).thenReturn(errorResponse)
test.callMethod()
assertValues(..,..,..)
I would test the above the code in below ways, i use mockito kotlin but i think this will help for what you are looking for i:e; right way ( that is subjective) :
#Test
fun `retry with newMyApiService when myAPI returns HTTP_UNAUTHORIZED`() {
myApi.stub {
on {
getCodeApi() } doReturn Erorr_Response_Model
}
newMyApiService.stub {
on {
getCodeApi() } doReturn Response_Model
}
test.callMethod();
verify(newMyApiService, times(1)). getCodeApi()
Assertions.assert(..Above Response_Model )
}
And a test to make sure that newAPIService does not always get called:
#Test
fun `myApi should return the valid result without retrying`() {
myApi.stub {
on {
getCodeApi() } doReturn SuccessModel
}
test.callMethod();
verify(newMyApiService, times(0)). getCodeApi()
verify(myApi, times(1)). getCodeApi()
Assertions.assert(..SuccessModel )
}
Please find below a function using a coroutine to replace callback :
override suspend fun signUp(authentication: Authentication): AuthenticationError {
return suspendCancellableCoroutine {
auth.createUserWithEmailAndPassword(authentication.email, authentication.password)
.addOnCompleteListener(activityLifeCycleService.getActivity()) { task ->
if (task.isSuccessful) {
it.resume(AuthenticationError.SignUpSuccess)
} else {
Log.w(this.javaClass.name, "createUserWithEmail:failure", task.exception)
it.resume(AuthenticationError.SignUpFail)
}
}
}
}
Now I would like to unit testing this function. I am using Mockk :
#Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() = runBlockingTest {
val listener = slot<OnCompleteListener<AuthResult>>()
val authentication = mockk<Authentication> {
every { email } returns "email"
every { password } returns "pswd"
}
val task = mockk<Task<AuthResult>> {
every { isSuccessful } returns true
}
every { auth.createUserWithEmailAndPassword("email", "pswd") } returns
mockk {
every { addOnCompleteListener(activity, capture(listener)) } returns mockk()
}
service.signUp(authentication)
listener.captured.onComplete(task)
}
Unfortunately this test failed due to the following exception : java.lang.IllegalStateException: This job has not completed yet
I tried to replace runBlockingTest with runBlocking but the test seems to wait in an infinite loop.
Can someone help me with this UT please?
Thanks in advance
As can be seen in this post:
This exception usually means that some coroutines from your tests were scheduled outside the test scope (more specifically the test dispatcher).
Instead of performing this:
private val networkContext: CoroutineContext = TestCoroutineDispatcher()
private val sut = Foo(
networkContext,
someInteractor
)
fun `some test`() = runBlockingTest() {
// given
...
// when
sut.foo()
// then
...
}
Create a test scope passing test dispatcher:
private val testDispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(testDispatcher)
private val networkContext: CoroutineContext = testDispatcher
private val sut = Foo(
networkContext,
someInteractor
)
Then in test perform testScope.runBlockingTest
fun `some test`() = testScope.runBlockingTest {
...
}
See also Craig Russell's "Unit Testing Coroutine Suspend Functions using TestCoroutineDispatcher"
In case of Flow testing:
Don't use flow.collect directly inside runBlockingTest. It should be wrapped in launch
Don't forget to cancel TestCoroutineScope in the end of a test. It will stop a Flow collecting.
Example:
class CoroutinesPlayground {
private val job = Job()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(job + testDispatcher)
#Test
fun `play with coroutines here`() = testScope.runBlockingTest {
val flow = MutableSharedFlow<Int>()
launch {
flow.collect { value ->
println("Value: $value")
}
}
launch {
repeat(10) { value ->
flow.emit(value)
delay(1000)
}
job.cancel()
}
}
}
This is not an official solution, so use it at your own risk.
This is similar to what #azizbekian posted, but instead of calling runBlocking, you call launch.
As this is using TestCoroutineDispatcher, any tasks scheduled to be run without delay are immediately executed. This might not be suitable if you have several tasks running asynchronously.
It might not be suitable for every case but I hope that it helps for simple cases.
You can also follow up on this issue here:
https://github.com/Kotlin/kotlinx.coroutines/issues/1204
If you know how to solve this using the already existing runBlockingTest and runBlocking, please be so kind and share with the community.
class MyTest {
private val dispatcher = TestCoroutineDispatcher()
private val testScope = TestCoroutineScope(dispatcher)
#Test
fun myTest {
val apiService = mockk<ApiService>()
val repository = MyRepository(apiService)
testScope.launch {
repository.someSuspendedFunction()
}
verify { apiService.expectedFunctionToBeCalled() }
}
}
According to my understanding, this exception occurs when you are using a different dispatcher in your code inside the runBlockingTest { } block with the one that started runBlockingTest { }.
So in order to avoid this, you first have to make sure you inject Dispatchers in your code, instead of hardcoding it throughout your app. If you haven't done it, there's nowhere to begin because you cannot assign a test dispatcher to your test codes.
Then, in your BaseUnitTest, you should have something like this:
#get:Rule
val coroutineRule = CoroutineTestRule()
#ExperimentalCoroutinesApi
class CoroutineTestRule(
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher()
) : TestWatcher() {
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.setMain(testDispatcher)
}
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
}
Next step really depends on how you do Depedency Injection. The main point is to make sure your test codes are using coroutineRule.testDispatcher after the injection.
Finally, call runBlockingTest { } from this testDispatcher:
#Test
fun `This should pass`() = coroutineRule.testDispatcher.runBlockingTest {
//Your test code where dispatcher is injected
}
There is an open issue for this problem: https://github.com/Kotlin/kotlinx.coroutines/issues/1204
The solution is to use the CoroutineScope intead of the TestCoroutinScope until the issue is resolved, you can do by replacing
#Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() =
runBlockingTest {
with
#Test
fun `signup() must be delegated to createUserWithEmailAndPassword()`() =
runBlocking {
None of these answers quite worked for my setup due to frequent changes in the coroutines API.
This specifically works using version 1.6.0 of kotlin-coroutines-test, added as a testImplementation dependency.
#Test
fun `test my function causes flow emission`() = runTest {
// calling this function will result in my flow emitting a value
viewModel.myPublicFunction("1234")
val job = launch {
// Force my flow to update via collect invocation
viewModel.myMemberFlow.collect()
}
// immediately cancel job
job.cancel()
assertEquals("1234", viewModel.myMemberFlow.value)
}
If you have any
Channel
inside the launch, you must call to
Channel.close()
Example code:
val channel = Channel<Success<Any>>()
val flow = channel.consumeAsFlow()
launch {
channel.send(Success(Any()))
channel.close()
}
runBlockingTest deprecated since 1.6.0 and replaced with runTest.
You need to swap arch background executor with one that execute tasks synchronously. eg. For room suspend functions, live data etc.
You need the following dependency for core testing
androidTestImplementation 'androidx.arch.core:core-testing:2.1.0'
Then add the following at the top of test class
#get:Rule
val instantExecutor = InstantTaskExecutorRule()
Explanations
InstantTaskExecutorRule A JUnit Test Rule that swaps the background executor used by the
Architecture Components with a different one which executes each task
synchronously.
You can use this rule for your host side tests that use Architecture
Components
As I mentioned here about fixing runBlockingTest, maybe it could help you too.
Add this dependency if you don't have it
testImplementation "androidx.arch.core:core-testing:$versions.testCoreTesting" (2.1.0)
Then in your test class declare InstantTaskExecutorRule rule:
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
I am creating a library and I'm using Retrofit with a call-adapter that gives me a Deferred<> value.
In a function in my code I call launch {}, and inside that i try-catch the values, and possible exceptions - calling different callbacks for different results.
The resources I've found on testing coroutines are all about testing suspended functions, and runBlocking {} is the solution to everything. Except for me it isn't
I made a quick example
#Mock
val mockListener: DoSomething.Listener = mock()
#Test
fun testSomething() {
val doer = DoSomething(mockListener)
runBlocking {
doer.doIt()
verify(mockListener).listen(any())
}
}
class DoSomething(val listener: Listener) {
interface Listener {
fun listen(s: String)
}
fun doIt() {
launch {
listener.listen(theThing().await())
}
}
private fun theThing(): Deferred<String> {
return async {
delay(5, TimeUnit.SECONDS)
return#async "Wow, a thing"
}
}
}
What I want is for the actually run all functions. The test should take 5 seconds minimum, but it just runs through the code in a couple of millisconds- ie. it doesn't block.
I've tried adding
runBlocking {
launch {
// doer.doIt()
}.joinChildren()
}
And similar practices but I just can't get the test to actually wait for my launch inside of another class to finish before the test is finished.
Placing the verify(...) outside of the runBlocking also makes the test fail, which it should.
Any input, helpers, good practice etc. is appreciated!
You can provide the CoroutineContext explicitly for your doIt() function:
fun doIt(context: CoroutineContext = DefaultDispatcher) {
launch(context) {
listener.listen(theThing().await()
}
}
With this parameter you could easily change the coroutine context - in your test code you use the blocking context:
runBlocking {
doer.doIt(coroutineContext)
}
BTW: You don't need to use launch and async. With launch you are in a suspendable context and you don't need to run theThing() asynchronously. Especially if you invoke await() in the next step:
fun doIt(context: CoroutineContext = DefaultDispatcher) {
launch(context) {
listener.listen(theThing())
}
}
private suspend fun theThing(): String {
delay(5, TimeUnit.SECONDS)
return "Wow, a thing"
}
Best way would be not to swallow Job in your doIt() function as you do now.
Instead of
fun doIt() {
launch {
listener.listen(theThing().await())
}
}
Do
fun doIt() = launch {
listener.listen(theThing().await())
}
That way your function will return a coroutine, which you can wait for:
doIt().join()
Better still is to use async() instead of launch()
Another comment is that doIt() should be actually doItAsync(), as suggested by Kotlin guidelines.