With migration to kotlin, view model and recent changes in [kotlin test lib][1] I am working on issue with test.
I have a scenario:
request a web resource asynchronously
in case of error put the request in cache and update state with new pending request
All of this with help of kotlin flow and view model.
Scenario works well when executes on emulator, but fails when I run test for it. The issue is catch block of flow has not been triggered when error has thrown in flow.
Here is the code:
fun mintToken(to: String, value: Value, uri: String) {
logger.d("[start] mintToken()")
viewModelScope.launch {
repository.mintToken(to, value, uri)
.catch { it ->
if (it is TransactionException
&& it.message!!.contains("Transaction receipt was not generated after 600 seconds for transaction")) {
cacheRepository.createChainTx(to, value, uri) // TODO consider always put in pending cache and remove after it confirms as succeeded
val txReceipt = TransactionReceipt()
txReceipt.transactionHash = ""
emit(Response.Data(txReceipt))
} else {
emit(Response.Error.Exception(it))
}
}
.flowOn(Dispatchers.IO)
.collect {
logger.d(it.toString())
when (it) {
is Response.Data -> {
if (it.data.transactionHash.isEmpty()) {
state.update {
it.copy(
status = Status.MINT_TOKEN,
pendingTx = it.pendingTx + Transaction(to, value, uri)
)
}
}
}
is Response.Error.Message -> {
val errorMsg = "Something went wrong on mint a token with error ${it.msg}"
logger.d(errorMsg)
state.update {
val newErrors = it.errors + "Something went wrong on mint a token with error ${errorMsg}"
it.copy(status = Status.MINT_TOKEN, errors = newErrors)
}
}
is Response.Error.Exception -> {
logger.e("Something went wrong on mint a token ${to}, ${value}, ${uri}", it.error)
state.update {
val newErrors = it.errors + "Something went wrong on mint a token ${to}, ${value}, ${uri}"
it.copy(status = Status.MINT_TOKEN, errors = newErrors)
}
}
}
}
}
logger.d("[end] mintToken()")
}
#Throws(TransactionException::class)
override fun mintToken(to: String, value: Value, uri: String): Flow<Response<TransactionReceipt>> {
return flow {
throw TransactionException(
"Transaction receipt was not generated after 600 seconds for transaction",
"")
}
}
Test code for this is:
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
// Set the main coroutines dispatcher for unit testing.
#ExperimentalCoroutinesApi
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
private lateinit var subj: WalletViewModel
#Test
fun `when mintToken() is called with correct values, timeout exception is returned and pending tx are updated with new value`() = runTest {
val to = "0x6f1d841afce211dAead45e6109895c20f8ee92f0"
val url = "https://google.com"
val testValue = Value(
"Software Development",
BigInteger.valueOf(1000L),
BigInteger.valueOf(2000L),
false,
BigInteger.valueOf(0)
)
subj.mintToken(to, testValue, url)
assertThat(
"There is no pending transaction after mint a new token with timeout error",
subj.uiState.value.pendingTx.isNotEmpty()
)
}
Test code differs from dev code by replacing dispatcher in MainCoroutineRule and using kotlin construction runTest {}. How does it affect this case? Does issue case lays in some other place?
[1]: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md
Related
Im currently trying to write test for my method that use shared flow object and get data from it. the problem is when in trying to set method return with mockito, shared flow not emit anything and after 1 minute test result is fails.
My test:
#ExperimentalCoroutinesApi
#Test
fun `when response body have error in request login`() = runBlockingTest {
runCurrent()
Mockito.`when`(webSocketClient.isConnect()).thenReturn(true)
Mockito.`when`(mapper.createRPC(userLoginObject)).thenReturn(rpc)
Mockito.`when`(requestManager.sendRequest(rpc)).thenReturn(userLoginFlow)
userLoginFlow.emit(errorObject)
loginServiceImpl.requestLogin(userLoginObject).drop(1).collectLatest {
assert(it == errorObject)
}
}
My method
override fun requestLogin(userLoginObject: BaseDomain): Flow<DataState<BaseDomain>> = flow {
emit(DataState.Loading(ProgressBarState.Loading))
if (webSocketClient.isConnect()) {
requestManager.sendRequest(mapper.createRPC(userLoginObject)!!)?.filterNotNull()?.collectLatest {
if (it is IG_RPC.Error) {
emit(DataState.Error(ErrorObject(it.major, it.minor, it.wait)))
} else if (it is IG_RPC.Res_User_Register) {
val userLoginObject = userLoginObject as UserLoginObject
emit(
DataState.Data(
UserLoginObject(
userName = it.userName,
phoneNumber = userLoginObject.phoneNumber,
userId = it.userId,
authorHash = it.authorHash,
regex = it.codeRegex,
resendCodeDelay = it.resendDelayTime
)
)
)
}
}
} else {
emit(DataState.Error(ErrorObject(-1, -1, 0)))
}
}
Error log:
This job has not completed yet
java.lang.IllegalStateException: This job has not completed yet
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 use KTor and Kotlin Serialization library in my android project, along with mockk and junit.jupiter for unit testing. I've encountered a problem when mocking ktor's suspend function readText(). The following unit test tests that initErrorMessage() function returns correct error message.
Test class:
class ErrorTest {
private val errorMessage = "objectId must be provided."
private val errorCode = 2689
private val correctResponseJson = "{\"code\":$errorCode,\"message\":\"$errorMessage\"}"
// ResponseException class is from ktor library
private val exceptionMock: ResponseException = mockk(relaxed = true)
#Test
fun `initErrorMessage should return correct error message`() = runTest {
coEvery { exceptionMock.response.readText() } returns correctResponseJson // <-- here is the Error occurs
val expectedError = errorMessage
val actualError = initErrorMessage(exceptionMock)
assertEquals(expectedError, actualError)
}
}
Method to test:
suspend fun initErrorMessage(cause: ResponseException): String {
return try {
val body = cause.response.readText()
val jsonSerializer = JsonObject.serializer()
val jsonObj = Json.decodeFromString(jsonSerializer, body)
jsonObj["message"].toString()
} catch (e: Exception) {
""
}
}
During execution of the first line in the test method I get an Error:
Premature end of stream: expected 1 bytes
java.io.EOFException: Premature end of stream: expected 1 bytes
at io.ktor.utils.io.core.StringsKt.prematureEndOfStream(Strings.kt:492)
at io.ktor.utils.io.core.internal.UnsafeKt.prepareReadHeadFallback(Unsafe.kt:78)
at io.ktor.utils.io.core.internal.UnsafeKt.prepareReadFirstHead(Unsafe.kt:61)
at io.ktor.utils.io.charsets.CharsetJVMKt.decode(CharsetJVM.kt:556)
at io.ktor.utils.io.charsets.EncodingKt.decode(Encoding.kt:103)
at io.ktor.utils.io.charsets.EncodingKt.decode$default(Encoding.kt:101)
at io.ktor.client.statement.HttpStatementKt.readText(HttpStatement.kt:173)
at io.ktor.client.statement.HttpStatementKt.readText$default(HttpStatement.kt:168)
at com.example.android.http.error.ErrorTest$initErrorMessage should return correct error message$1$1.invokeSuspend(ErrorTest.kt:37)
at com.example.android.http.error.ErrorTest$initErrorMessage should return correct error message$1$1.invoke(ErrorTest.kt)
at com.example.android.http.error.ErrorTest$initErrorMessage should return correct error message$1$1.invoke(ErrorTest.kt)
at io.mockk.impl.eval.RecordedBlockEvaluator$record$block$2$1.invokeSuspend(RecordedBlockEvaluator.kt:28)
at io.mockk.impl.eval.RecordedBlockEvaluator$record$block$2$1.invoke(RecordedBlockEvaluator.kt)
at io.mockk.InternalPlatformDsl$runCoroutine$1.invokeSuspend(InternalPlatformDsl.kt:20)
How to mock this suspend method readText() without an Error?
It turned out that the function readText() hasn't been mocked properly.
It is an extension function on HttpResponse and it has to be mocked using mockkStatic function, for example like this:
#BeforeEach
fun setup() {
mockkStatic(HttpResponse::readText)
}
setup() will be executed before each #Test, because it is marked with #BeforeEach annotation.
You can mock the HttpClientCall instead of the ResponseException to create an instance of the ResponseException without mocking (to avoid EOFException).
class ErrorTest {
private val errorMessage = "objectId must be provided."
private val errorCode = 2689
private val correctResponseJson = "{\"code\":$errorCode,\"message\":\"$errorMessage\"}"
#OptIn(InternalAPI::class)
#Test
fun `initErrorMessage should return correct error message`(): Unit = runBlocking {
val responseData = HttpResponseData(
statusCode = HttpStatusCode.OK,
requestTime = GMTDate.START,
headers = Headers.Empty,
version = HttpProtocolVersion.HTTP_1_1,
"",
coroutineContext
)
val call = mockk<HttpClientCall>(relaxed = true) {
// This is how a body received under the hood
coEvery { receive<Input>() } returns BytePacketBuilder().apply { writeText(correctResponseJson) }.build()
// There are cyclic dependencies between HttpClientCall and HttpResponse so it's not possible to mock it in place
every { response } returns DefaultHttpResponse(this, responseData)
}
val exception = ResponseException(call.response, "")
val expectedError = errorMessage
val actualError = initErrorMessage(exception)
assertEquals(expectedError, actualError)
}
}
My test case to test the viewmodel looks like this :
#Before
fun setUp() {
loginActivityViewModel = LoginActivityViewModel(loginRepository)
.apply { users.observeForever(userObserver) }
}
#Test
fun `check user response when get successful response from server`() {
testCoroutineRule.runBlockingTest {
//Given
whenever(loginRepository.getLoginResponse(loginRequest)).then(Answer { loginResponse })
//When
loginActivityViewModel.loginResponse(loginRequest)
//Then
verify(userObserver).onChanged(Resource.loading(data = null))
verify(userObserver).onChanged(Resource.success(data = loginResponse))
}
}
#Test
fun `check user response when get unsuccessful response from server`() {
testCoroutineRule.runBlockingTest {
//Given
whenever(loginRepository.getLoginResponse(loginRequest)).thenThrow(Error("Some error"))
//When
loginActivityViewModel.loginResponse(loginRequest)
//Then
verify(userObserver).onChanged(Resource.loading(data = null))
verify(userObserver).onChanged(Resource.error(message = "Some error"))
}
}
Inside this first test case run successfully but when it run 2nd one giving this error:
Wanted but not invoked: userObserver.onChanged(
Resource(status=ERROR, data=null, message=Some error) );
-> at com.android.loginapp.viewmodel.LoginActivityViewModelTest$check user response when get unsuccessful response from
server$1.invokeSuspend(LoginActivityViewModelTest.kt:83)
However, there was exactly 1 interaction with this mock:
userObserver.onChanged(
Resource(status=LOADING, data=null, message=null) );
-> at androidx.lifecycle.LiveData.considerNotify(LiveData.java:131)
My viewModel network calling method look like this:
fun loginResponse(loginRequest: LoginRequest) {
viewModelScope.launch {
users.postValue(Resource.loading(null))
try {
val usersFromApi = loginRepository.getLoginResponse(loginRequest)
users.postValue(Resource.success(usersFromApi))
} catch (e: Exception) {
users.postValue(Resource.error(e.message.toString()))
}
}
}
Not sure why it's giving this error.
I need use .thenThrow(RuntimeException("test error")) then only it will pass.
I made app where user can add server (recycler row) to favorites. It only saves the IP and Port. Than, when user open FavoriteFragment Retrofit makes calls for each server
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Server
So in repository I mix the sources and make multiple calls:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
list.add(server)
}
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
and then in ViewModel I create LiveData object
fun getFavoriteServers() {
viewModelScope.launch {
repository.getFavoriteServersToRecyclerView()
.onEach { dataState ->
_favoriteServers.value = dataState
}.launchIn(viewModelScope)
}
}
And everything works fine till the Favorite server is not more available in the Lobby and the Retrofit call failure.
My question is: how to skip the failed call in the loop without crashing whole function.
Emit another flow in catch with emitAll if you wish to continue flow like onResumeNext with RxJava
catch { cause ->
emitAll(flow { emit(DataState.Errorcause)})
}
Ok, I found the solution:
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val list: MutableList<Server> = mutableListOf()
try {
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
val job = CoroutineScope(coroutineContext).launch {
getFavoritesServersNotLiveData.forEach { fav ->
val server = getServer(fav.ip, fav.port)
server.collect { dataState ->
when (dataState) {
is DataState.Loading -> Log.d(TAG, "loading")
is DataState.Error -> Log.d(TAG, dataState.exception.message!!)
is DataState.Success -> {
list.add(dataState.data)
Log.d(TAG, dataState.data.toString())
}
}
}
}
}
job.join()
emit(DataState.Success(list))
} catch (e: Exception) {
emit(DataState.Error(e))
}
}
when using retrofit you can wrap response object with Response<T> (import response from retrofit) so that,
#GET("v0/server/{ip}/{port}")
suspend fun getServer(
#Path("ip") ip: String,
#Path("port") port: Int
): Response<Server>
and then in the Repository you can check if network failed without using try-catch
suspend fun getFavoriteServersToRecyclerView(): Flow<DataState<List<Server>>> = flow {
emit(DataState.Loading)
val getFavoritesServersNotLiveData = favoritesDao.getFavoritesServersNotLiveData()
if(getFavoritesServersNotLiveData.isSuccessful) {
val list: MutableList<Server> = mutableListOf()
getFavoritesServersNotLiveData.body().forEach { fav ->
val server = soldatApiService.getServer(fav.ip, fav.port)
// if the above request fails it wont go to the else block
list.add(server)
}
emit(DataState.Success(list))
} else {
val error = getFavoritesServersNotLiveData.errorBody()!!
//do something with error
}
}