how to throw an exception on a retrofit call - android

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

Related

How I can mock response of Apollo Android

I'm trying to mock response to my query but I can't because the builder needed and doesn't know how to pass operations?
Basicly I have network class like this :
class NetworkService #Inject constructor(
private val apolloClient: ApolloClient
) {
suspend fun <D : Operation.Data, T, V : Operation.Variables> suspendedQuery(
query: Query<D, T, V>,
cachePolicy: HttpCachePolicy.Policy = HttpCachePolicy.NETWORK_FIRST
): Resource<Response<T>> {
val response = try {
apolloClient.query(query)
.toBuilder().httpCachePolicy(cachePolicy)
.build()
.await()
} catch (e: ApolloException) {
return Resource.error(e.localizedMessage)
}
return if (response.hasErrors()) {
Resource.error(response.errors.toString())
} else {
Resource.success(response)
}
}
}
and I want to mock the response returned from this function like this
I successed when I'm returning an error
val expectedResponse = Resource.error<Response<MyQuery.Data>>("ERROR")
But I have problem to mocking the resposne here:
val expectedResponse = Resource.success<Response<MyQuery.Data>>(Response("Response.builder(Operation<>)"))
I want to know How to build the inside the quote "Response.builder(Operation<>)"?
It would appear that this is a long running problem according to the apollo-android issues list. I can see that you've asked a similar question there also.
I found this issue which looks to be resolving this problem in the not too distant future. It was supposed to be this month but now looks like next.
So think this means that you're only solution right now is to use mock web server, and have it return a sample response to the client. This is how we have implemented our tests currently.

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.

Unit test two api calls within a method

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

Suspending function test with MockWebServer

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.

Exception not being caught in Coroutines

I can't seem to get my error-handling done in coroutines. I've been reading lots of articles and the exception handling documentation but I can't seem to get it working.
Here's my setup:
My ViewModel launches the coroutine with it's scope
class MyViewModel(private var myUseCase: MyUseCase) : ViewModel() {
private val viewModelJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob)
fun doSomething() {
uiScope.launch {
try {
myUseCase()
} catch (exception: Exception) {
// Do error handling here
}
}
}
}
My UseCase just handles a few logic and in this case a validator of some sort
class MyUseCase(private val myRepository: MyRepository) {
suspend operator fun invoke() {
if (checker()) {
throw CustomException("Checker Failed due to: ...")
}
myRepository.doSomething()
}
}
Then my Repository just handles the network layer / local layer
object MyRepository {
private val api = ... // Retrofit
suspend fun doSomething() = api.doSomething()
}
And here's my Retrofit interface
interface MyInterface {
#POST
suspend fun doSomething()
}
The try/catch from the ViewModel can handle the error from the Retrofit call however, it can't catch the error from the CustomException thrown by the UseCase. From articles I've been reading, this should work. If I use async I can do await and consume the error but I don't have to use async in this case and I've been wrapping my head around this. I might be getting lost.
Any help would be greatly appreciated! Thanks in advance!
Edit:
Here's the error log I'm getting:
com.example.myapp.domain.errors.CustomException
at com.example.myapp.domain.FeatureOne$invoke$2.invokeSuspend(FeatureOne.kt:34)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
The error directly points to the explicit throw statement.
Trying with CoroutineExceptionHandler can be workaround for handling exceptions inside coroutines.
CoroutineExceptionHandler context element is used as generic catch block of coroutine where custom logging or exception handling may take place. It is similar to using Thread.uncaughtExceptionHandler.
How to use it?
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
val job = GlobalScope.launch(handler) {
throw AssertionError()
}
val deferred = GlobalScope.async(handler) {
throw ArithmeticException() // Nothing will be printed, relying on user to call
deferred.await()
}
joinAll(job, deferred)
In your ViewModel, make sure that your uiScope is using SupervisorJob rather than Job. SupervisorJob's can handle its children's failure individually. Job would get cancelled unlike SupervisorJob
If you're using 2.1.0 for AAC Lifecycle and ViewModel, use the viewModelScope extension instead.
Another way to resolve this would be to covert your custom error object to implement CancellationException
For eg:
Your CustomException can be implemented as :
sealed class CustomError : CancellationException() {
data class CustomException(override val message: String = "Checker Failed due to: ...") : CustomError
}
This exception would get caught in the try/catch block of the view model
As far as I know Retrofit hasn't still created the way to mark the methods with the suspend keyword. You can refer it to this link.
So the correct way of your MyInterface would be:
interface MyInterface {
#POST
fun doSomething(): Deferred<Response<YourDataType>>
}

Categories

Resources