Suspending function test with MockWebServer - android

I'm testing api that returns result using suspending function with MockWebServer, but it does not work with runBlockingTest, testCoroutineDispatcher, testCorounieScope unless a launch builder is used, why?
abstract class AbstractPostApiTest {
internal lateinit var mockWebServer: MockWebServer
private val responseAsString by lazy {
getResourceAsText(RESPONSE_JSON_PATH)
}
#BeforeEach
open fun setUp() {
mockWebServer = MockWebServer()
println("AbstractPostApiTest setUp() $mockWebServer")
}
#AfterEach
open fun tearDown() {
mockWebServer.shutdown()
}
companion object {
const val RESPONSE_JSON_PATH = "posts.json"
}
#Throws(IOException::class)
fun enqueueResponse(
code: Int = 200,
headers: Map<String, String>? = null
): MockResponse {
// Define mock response
val mockResponse = MockResponse()
// Set response code
mockResponse.setResponseCode(code)
// Set headers
headers?.let {
for ((key, value) in it) {
mockResponse.addHeader(key, value)
}
}
// Set body
mockWebServer.enqueue(
mockResponse.setBody(responseAsString)
)
return mockResponse
}
}
class PostApiTest : AbstractPostApiTest() {
private lateinit var postApi: PostApiCoroutines
private val testCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
#BeforeEach
override fun setUp() {
super.setUp()
val okHttpClient = OkHttpClient
.Builder()
.build()
postApi = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
.create(PostApiCoroutines::class.java)
Dispatchers.setMain(testCoroutineDispatcher)
}
#AfterEach
override fun tearDown() {
super.tearDown()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
#Test
fun `Given we have a valid request, should be done to correct url`() =
testCoroutineScope.runBlockingTest {
// GIVEN
enqueueResponse(200, RESPONSE_JSON_PATH)
// WHEN
postApi.getPostsResponse()
advanceUntilIdle()
val request = mockWebServer.takeRequest()
// THEN
Truth.assertThat(request.path).isEqualTo("/posts")
}
}
Results error: java.lang.IllegalStateException: This job has not completed yet
This test does not work if launch builder is used, and if launch builder is used it does not require testCoroutineDispatcher or testCoroutineScope, what's the reason for this? Normally suspending functions pass without being in another scope even with runBlockingTest
#Test
fun `Given we have a valid request, should be done to correct url`() =
runBlockingTest {
// GIVEN
enqueueResponse(200, RESPONSE_JSON_PATH)
// WHEN
launch {
postApi.getPosts()
}
val request = mockWebServer.takeRequest()
// THEN
Truth.assertThat(request.path).isEqualTo("/posts")
}
The one above works.
Also the test below pass some of the time.
#Test
fun Given api return 200, should have list of posts() =
testCoroutineScope.runBlockingTest {
// GIVEN
enqueueResponse(200)
// WHEN
var posts: List<Post> = emptyList()
launch {
posts = postApi.getPosts()
}
advanceUntilIdle()
// THEN
Truth.assertThat(posts).isNotNull()
Truth.assertThat(posts.size).isEqualTo(100)
}
I tried many combinations invoking posts = postApi.getPosts() without launch, using async, putting enqueueResponse(200) inside async async { enqueueResponse(200) }.await() but tests failed, sometimes it pass sometimes it does not some with each combination.

There is a bug with runBlockTest not waiting for other threads/jobs to complete before completing the coroutine that the test is running in.
I tried using runBlocking with success (I use the awesome port of Hamcrest to Kotlin Hamkrest)
fun `run test` = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(""))
// make HTTP call
val result = mockWebServer.takeRequest(2000L, TimeUnit.MILLISECONDS)
assertThat(result != null, equalTo(true))
}
There's a few things to note here:
The use of thread blocking calls should never be called without a timeout. Always better to fail with nothing, then to block a thread forever.
The use of runBlocking might be considered by some to be no no. However this blog post outlines the different method of running concurrent code, and the different use cases for them. We normally want to use runBlockingTest or (TestCoroutineDispatcher.runBlockingTest) so that our test code and app code are synchronised. By using the same Dispatcher we can make sure that the jobs all finish, etc. TestCoroutineDispatcher also has that handy "clock" feature to make delays disappear. However when testing the HTTP layer of the application, and where there is a mock server running on a separate thread we have a synchronisation point being takeRequest. So we can happily use runBlocking to allow us to use coroutines and a mock server running on a different thread work together with no problems.

Related

how to throw an exception on a retrofit call

I am making an api call using retrofit and I want to write a unit test to check if it returns an exception.
I want to force the retrofit call to return an exception
DataRepository
class DataRepository #Inject constructor(
private val apiServiceInterface: ApiServiceInterface
) {
suspend fun getCreditReport(): CreditReportResponse {
try {
val creditReport = apiServiceInterface.getDataFromApi() // THIS SHOULD RETURN AN EXCEPTION AND I WANT TO CATCH THAT
return CreditReportResponse(creditReport, CreditReportResponse.Status.SUCCESS)
} catch (e: Exception) {
return CreditReportResponse(null, CreditReportResponse.Status.FAILURE)
}
}
}
ApiServiceInterface
interface ApiServiceInterface {
#GET("endpoint.json")
suspend fun getDataFromApi(): CreditReport
}
I have written a test case for getCreditReport which should validate the failure scenario
#Test
fun getCreditReportThrowException() {
runBlocking {
val response = dataRepository.getCreditReport()
verify(apiServiceInterface, times(1)).getDataFromApi()
Assert.assertEquals(CreditReportResponse.Status.FAILURE, response.status)
}
}
so to make the above test case pass, I need to force the network call to throw and exception
please suggest
Thanks
R
Actually #Vaibhav Goyal provided a good suggestion to make your testing as easier. Assuming you are using MVVM structure, in your test cases you can inject a "mock" service class to mock the behaviours that you defined in the test cases, so the graph will be like this
Since I am using mockk library at the moment, the actual implementation in your code base would be a little bit different.
#Test
fun test_exception() {
// given
val mockService = mockk<ApiServiceInterface>()
val repository = DataRepository(mockService)
every { mockService.getDataFromApi() } throws Exception("Error")
// when
val response = runBlocking {
repository.getCreditReport()
}
// then
verify(exactly = 1) { mockService.getDataFromApi }
assertEquals(CreditReportResponse.Status.FAILURE,response.status)
}
But if you want to test the exception thrown from Retrofit, then you might need mockServer library from square to help you to achieve this https://github.com/square/okhttp#mockwebserver
And the graph for this would be like this
You also have to setup the mock server to do so
#Test
fun test_exception_from_retrofit() {
// can put in the setup method / in junit4 rule or junit5 class
val mockWebServer = MockWebServer()
mockWebServer.start()
// given
val service = Retrofit.Builder()
.baseUrl(mockWebServer.url("/").toString())
.build()
.create(ApiServiceInterface::class)
val repository = DataRepository(service)
// when
mockWebServer.enqueue(MockResponse()
.setResponseCode(500)
.setBody("""{"name":"Tony}""") // you can read the json file content and then put it here
)
val response = runBlocking {
repository.getCreditReport()
}
// then
verify(exactly = 1) { mockService.getDataFromApi }
assertEquals(CreditReportResponse.Status.FAILURE,response.status)
// can put in tearDown / in junit4 rule or juni5 class
mockWebServer.shutdown()
}
SO you can test different exception like json format invalid, 500 status code,data parsing exception
Bonus point
Usually I would put the testing json under test directory and make it almost same as the api path for better maintainence

How to mock API's before launching the UI test?

I want to mock the API response before the activity launch to test the real-time scenario. I am using rxjava with viewmodel to make the API calls in actual activity class. I have created a custom dispatcher to mock the API calls and calling it in BaseUItest class. But this process is not working when i launch the test as i see no data is loading up and no mock API call is being made. Here's the BaseUItest:
open class BaseUITest {
protected var mockWebServer = MockWebServer()
#Before
open fun setup() {
mockWebServer.start(5000)
mockWebServer.dispatcher = ApiDispatcher()
}
#After
open fun teardown() {
mockWebServer.shutdown()
}
}
Do i need to mock viewmodels too before mocking the API's? What I am doing here? Please help.
In the dispatcher you need to check for the end point, and produce your own mock response for this mock server. For example.
val server = MockWebServer()
val dispatcher: Dispatcher = object : Dispatcher() {
#Throws(InterruptedException::class)
override fun dispatch(request: RecordedRequest): MockResponse? {
when (request.path) {
"/v1/login/auth/" -> return MockResponse().setResponseCode(200)
"/v1/profile/info" -> return MockResponse().setResponseCode(200)
.setBody("{\"info\":{\"name\":\"Lucas Albuquerque\",\"age\":\"21\",\"gender\":\"male\"}}")
}
return MockResponse().setResponseCode(404)
}
}
server.setDispatcher(dispatcher)
In the calling section you can get the endpoint url by
fun getUrl(): HttpUrl {
return server.url("/v1/profile/info")
}
Which will return the server response with json body as mentioned in the dispatcher.

How to only test the Retrofit request param in MockWebServer without actually execute the request?

I know it's possible to test Retrofit request & response with MockWebServer, like this:
interface AppApi {
#GET("/time/")
suspend fun time(): TimeResponse
}
...
class CoinBaseApiClientTest {
private val mockWebServer = MockWebServer()
private fun createClient(): AppApi {
return AppApiFactory.createAppApi(baseUrl = mockWebServer.url("/").toString())
}
#Before
fun setUp() {
mockWebServer.start()
}
#After
fun tearDown() {
mockWebServer.shutdown()
}
#Test
fun fetches_time() = runBlocking {
val timeData: String = """
{
"iso": "2015-01-07T23:47:25.201Z",
"epoch": 1420674445.201
}
"""
mockWebServer.enqueue(MockResponse().mockSuccess(200, timeData))
val timeResponse = createClient().time()
val recordedRequest = mockWebServer.takeRequest()
assertThat(recordedRequest.path).isEqualTo("/time/")
assertThat(timeResponse.iso).isEqualTo("2015-01-07T23:47:25.201Z")
assertThat(timeResponse.epochAsMillis).isEqualTo(1420674445201)
}
However, in my case, I only want to test its request payload, such as path, header... without actually execute the time() API (the reason is the actual timeData is really big). So I set mockWebServer.enqueue(MockResponse()) but it doesn't work - it seems require valid TimeResponse JSON data.
Do you know is it possible to only test Retrofit request payload without actually execute the request?
You could just have it return an error status code (i.e. 500) with empty data.

Unit testing coroutines runBlockingTest: This job has not completed yet

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()

Android Mockito Kotlin coroutine test cancel

I have a retrofit service
interface Service {
#PUT("path")
suspend fun dostuff(#Body body: String)
}
It is used in android view model.
class VM : ViewModel(private val service: Service){
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
val state = MutableLiveData<String()
init {
uiScope.launch {
service.doStuff()
state.value = "lol"
}
}
override fun onCleared(){
viewModelJob.cancel()
}
}
I would like to write a test for the cancelling of the view model. This will be done mocking service and delaying so that the co routine does not complete. Whilst blocking, we invoke onCleared to cancel the co routine. This should prevent state getting set...
#Test
fun `on cleared - cancels request`() = runBlocking {
//given
`when`(service.doStuff()).thenAnswer { launch { delay(1000) } }
val vm = ViewModel(service)
// when
vm.cleared()
//then
assertThat(vm.state, nullValue())
}
However it seems that vm.state always gets set??? What is the best way to test when clearing a scope that a co routine gets cancelled?
The problem here is in thenAnswer { launch { delay(1000) } }, which effectively makes your doStuff method look like that:
suspend fun doStuff() {
launch { delay(1000) }
}
As you can see, this function does not actually suspend, it launches a coroutine and returns immediately. What would actually work here is thenAnswer { delay(1000) }, which does not work, because there is no suspend version of thenAnswer in Mockito (as far as I know at least).
I would recommend to switch to Mokk mocking library, which supports kotlin natively. Then you can write coEvery { doStuff() } coAnswers { delay(1000) } and it will make your test pass (after fixing all the syntax errors ofc).

Categories

Resources