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.
Related
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
Had trouble even finding some useful resources on Firebase+Retrofit (contrary to overpopulated tutorials on other aspects of Android). First time using Firebase, i've imported a json:
{"Qoutes" :[
{
"quote":"I\u2019m sure you are tired counting the years of your age. May these years be endless and full of happiness!",
"relation":"grandmother"
},
{
"quote":"My sweet Grandma, I wish you to be always happy and healthy as today! Happy birthday my beloved old woman!",
"relation":"grandmother"
},
{
"quote":"May you stay healthy and happy for the years to come. We are all happy to have you here with us!",
"relation":"grandmother"
},
.
.
.
]}
Wish class
data class Wish(
#SerializedName("quote")
val quote: String?,
#SerializedName("relation")
val relation: String?
)
APIService interface
interface APIService {
#GET("Qoutes")
suspend fun getQuotes(): Response<Wish>
}
RetrofitInstance
object RetrofitInstance {
private val retrofit: Retrofit by lazy {
Retrofit.Builder()
.baseUrl("https://birthdayzheimer-default-rtdb.firebaseio.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val api: APIService by lazy {
retrofit.create(APIService::class.java)
}
}
Repository
class Repository #Inject constructor() {
suspend fun getWishes(): Response<Wish> {
return RetrofitInstance.api.getQuotes()
}
}
ViewModel
private val repository: Repository
): ViewModel() {
var myResponse: MutableLiveData<Response<Wish>> = MutableLiveData()
fun getQuotes() {
viewModelScope.launch {
val response: Response<Wish> = repository.getWishes()
myResponse.value = response
}
}
and the relevant part inside the fragment
viewModel.getQuotes()
viewModel.myResponse.observe(viewLifecycleOwner, { response ->
if(response.isSuccessful)
Log.d("Response ->", response.body().toString())
else
Log.d("Response ->", response.errorBody().toString())
I hope that it's a noobish mistake somewhere as this is all fresh to me, and that it's not too bothersome to read all the classes. Thanks in advance to everyone reading.
P.s. i know i wrote quotes wrong in json, so i was consistent and wrote it wrongfully same everywhere else :D
What you're trying to do here is access the Firebase Realtime Database through its REST API. To make sure this works, you need to end the URL with .json
So:
Retrofit.Builder()
.baseUrl("https://birthdayzheimer-default-rtdb.firebaseio.com/.json")
Note that there might be multiple problems, but this is certainly one of them.
I have a Client that will make a request to a GraphQL end point as below
import com.apollographql.apollo.ApolloClient
internal class GraphQLClient(apolloClient:ApolloClient, retryStrategy:RetryStrategy){
override fun <D : Operation.Data, T, V : Operation.Variables> mutate(mutate: Mutation<D, T, V>): Flow<T> {
val response = apolloClient.mutate(mutate)
val responseFlow = response.toFlow()
return responseFlow.map<Response<T>, T> {
it.data ?: throw ResponseError("Data Null")
}.retryWhen { cause, attempt ->
retryStrategy.retry(cause,attempt)
}
}
}
I'm testing the above using MockWebServer to create mock responses
JUnit test
Using Turbine to test Flow
I'm trying to validate that for a successful update request the retry logic doesn't get executed
#Test
fun `GIVEN a successful update, THEN don't retry`() = runBlocking {
val server = MockWebServer()
val mockResponse = MockResponse()
//Successful response in json format. It's the correct format.
mockResponse.setBody(readFileFromResources("mock_success_response.json"))
mockResponse.setResponseCode(200)
server.enqueue(mockResponse)
server.start()
val url = server.url("http://loalhost:8080")
val apolloClient: ApolloClient = ApolloClient.builder()
.okHttpClient(OkHttpClient())
.serverUrl(url.toString())
.addCustomAdapters()
.build()
val retryStrategy = mockk<RetryStrategy>()
val graphQLClient = GraphQLClient(apolloClient)
//The mutation of intrest
val mutation = UpdateMutation(
SomeInput(
"123"
)
)
//note how i haven't mocked anything related to retry strategy cause this test doesn't need that
graphQLClient.mutate(mutation).test {
verify(exactly = 0) { retryStrategy.retry(any(),any()) }
expectComplete()
}
server.shutdown()
}
However, my test fails with
app.cash.turbine.AssertionError: Expected complete but found Error(MockKException)
Further down the stack trace, I can see the complaint over the lack of answer for some re-try logic related things
But i think that's a cause of the above exception being thrown and in reality, shouldn't even be executed
Caused by: io.mockk.MockKException: no answer found for: RetryStrategy(#1).isError(com.apollographql.apollo.exception.ApolloNetworkException: Failed to execute http call)
P:S- I'm probably testing a little too much here too but keen to understand what's going on
Things I've tried
Just changed the response to an empty string if that has an impact but no change in error. Which makes me think it probably has nothing to do with the response data,
Thanks
The problem was with this line
val url = server.url("http://loalhost:8080")
MockServer is not expecting host or port
val url = server.url("/somepath")
I'm finding this exception related with Moshi sometimes when opening the app:
Caused by java.lang.ArrayIndexOutOfBoundsException: length=33; index=33
at java.util.ArrayList.add(ArrayList.java:468)
at com.squareup.moshi.Moshi$Builder.add(Moshi.java:231)
We initialise a repository in the BaseApplication which, sometimes, results in the mentioned crash when initialising Moshi. I'm finding this error in the app reports but I'm not able to reproduce it. Let's jump to the what we have and see if you might have a clue on it.
This factory is used to create Moshi instances, getting the crash when adding KotlinJsonAdapterFactory:
object MyMoshiConverterFactory {
fun create(setup: (Moshi.Builder.() -> Unit)? = null): Converter.Factory {
val moshi = MoshiUtil.createMoshi()
setup?.let { moshi.it() }
moshi.add(KotlinJsonAdapterFactory()) // Here is the crash!
return MoshiConverterFactory.create(moshi.build())
}
}
Here we have a class where we have all the converters we use. It really has a lot more of converters, but I've removed a few of them for simplicity:
object MoshiUtil {
private val lazyMoshi by lazy {
Moshi.Builder().apply {
add(DateAdapter())
add(DefaultOnDataMismatchAdapter.newFactory(FeedItem::class.java, null))
add(SkipListNullValuesAdapter.createFactory(Element::class.java))
add(SkipListNullValuesAdapter.createFactory(Post::class.java))
add(SkipListNullValuesAdapter.createFactory(MetadataItem::class.java))
add(GeoGeometry::class.java, GeometryAdapter())
}
}
fun createMoshi() = lazyMoshi
}
And finally, in our BaseApplication, we have something like this:
class BaseApplication {
#Override
public void onCreate() {
super.onCreate();
val myService = getMyService(applicationContext)
}
private fun getMyService(appContext: Context): MyService {
val converterFactory = MyMoshiConverterFactory.create()
return Retrofit.Builder().baseUrl(baseUrl).apply {
addConverterFactory(converterFactory)
client(okHttpClientBuilder.build())
}.build().create(MyService::class.java)
}
}
}
So, do you see anything that could be causing it? Do you think it might be a concurrency issue happening at startup when the several places in the app are creating the MoshiUtils object at the same time?. Looking forward to hear from you guys, thanks!
Moshi.Builder is mutable and not thread-safe, so this error you're getting sometimes is a race condition as a result of that. You should call .build() on that base MoshiUtil instance to get an immutable Moshi instance, then make the return value of MoshiUtil.createMoshi be moshi.newBuilder() (creates a Moshi.Builder already configured like the existing Moshi instance), like so:
object MoshiUtil {
private val baseMoshi: Moshi = Moshi.Builder().apply {
// ...
}.build()
fun createMoshi(): Moshi.Builder = baseMoshi.newBuilder()
}
Since every person that calls createMoshi now gets their own instance of Moshi.Builder, there shouldn't be any concurrency problems anymore.
This scene takes place in an Android app using Retrofit2 and Moshi for JSON deserialization.
In a case where you don't have control over the server's implementation, and this said server have an inconsistent behavior in how it answers requests (also know as "a bad case"):
Is there a way to handle com.squareup.moshi.JsonDataException without crashing?
For example you expected a JSONArray, and here comes a JSONObject. Crash. Is there another way to handle this than having the app crashing?
Also in the case the server's implementation is updated, wouldn't it be better to display an error message to the user, instead of having it to crash / be totally out of service, even for one wrong request?
Make the call with Retrofit and use try and catch to handle exceptions, something similar to:
class NetworkCardDataSource(
private val networkApi: NetworkCardAPI,
private val mapper: CardResponseMapper,
private val networkExceptionMapper: RetrofitExceptionMapper,
private val parserExceptionMapper: MoshiExceptionMapper
) : RemoteCardDataSource {
override suspend fun getCard(id: String): Outcome<Card, Throwable> = withContext(Dispatchers.IO) {
val response: Response<CardResponseJson>
return#withContext try {
response = networkApi.getCard(id)
handleResponse(
response,
data = response.body(),
transform = { mapper.mapFromRemote(it.card) }
)
} catch (e: JsonDataException) {
// Moshi parsing error
Outcome.Failure(parserExceptionMapper.getException(e))
} catch (e: Exception) {
// Retrofit error
Outcome.Failure(networkExceptionMapper.getException(e))
}
}
private fun <Json, D, L> handleResponse(response: Response<Json>, data: D?, transform: (D) -> L): Outcome<L, Throwable> {
return if (response.isSuccessful) {
data?.let {
Outcome.Success(transform(it))
} ?: Outcome.Failure(RuntimeException("JSON cannot be deserialized"))
} else {
Outcome.Failure(
HTTPException(
response.message(),
Exception(response.raw().message),
response.code(),
response.body().toString()
)
)
}
}
}
where:
networkApi is your Retrofit object,
mapper is a class for mapping the received object to another one used in your app (if needed),
networkExceptionMapper and parserExceptionMapper map Retrofit and Moshi exceptions, respectively, to your own exceptions so that Retrofit and Moshi exceptions do not spread all over your app (if needed),
Outcome is just a iOS Result enum copy to return either a Success or a Failure result but not both,
HTTPException is a custom Runtime exception to return unsuccessful request.
This a snippet from a clean architecture example project.