Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enqueued MockServer response null #5571

Closed
rb090 opened this issue Jan 26, 2024 · 10 comments
Closed

Enqueued MockServer response null #5571

rb090 opened this issue Jan 26, 2024 · 10 comments

Comments

@rb090
Copy link

rb090 commented Jan 26, 2024

Question

Hi 👋,

I am trying to write some unit tests for my Apollo code and I want to use this nice MockServer. Nonetheless, when enqueue responses to it and execute my "mock query", I end up in ApolloResponse with data and errors null.

I am using com.apollographql.apollo3:apollo-testing-support v4.0.0-beta.4 and com.apollographql.apollo3:apollo-mockserver v4.0.0-beta.4

And I don't know why. Can you please have a quick look and tell me what I am doing wrong?

// Create apollo mock server
val mockServer = MockServer()

// Enqueue HTTP responses
mockServer.enqueue(
    mockResponse = MockResponse.Builder().statusCode(statusCode = 200)
        .headers(headers = headers)
        .body(body = """
              {
                "data": {
                    "getUser": {
                        "birthdate": "1990-11-08T00:00:00",
                        "city": null,
                        "country": null,
                        "deactivationTimestamp": null,
                        "email": "roxana@company.de",
                        "firstName": "Roxana",
                        "gender": "FEMALE",
                        "houseNr": null,
                        "id": "xxxx",
                        "lastName": "Test",
                        "phone": null,
                        "postalCode": null,
                        "salutation": "MRS",
                        "street": null,
                        "title": null,
                        "lastOnline": "2024-01-25T18:18:52"
                    }
                }
              }
        """)
        .build()
)

val apolloClient = ApolloClient.Builder().serverUrl(mockServer.url()).build()

val receivedUser = apolloClient.query(GetUserQuery()).execute()

mockServer.close()

// This shows me both attributes as null
println("receivedUser.data: ${receivedUser.data}")
println("receivedUser.errors: ${receivedUser.errors}")
@martinbonnin
Copy link
Contributor

martinbonnin commented Jan 26, 2024

Hi 👋

Can you share your GetUserQuery? If it has any non-null fields that are not named random, it'll fail parsing and you should have an exception in receivedUser.exception.

@rb090
Copy link
Author

rb090 commented Jan 26, 2024

Hi @martinbonnin,

thanks a lot for your answer on this. I do not get an exception. I corrected the body in my posted message and replaced with the actual body of my GetUserQuery. I used for this GitHub issue here initially a "test string".

My GetUserQuery looks like this:

query GetUser {
    getUser {
        ...MyUser
    }
}

fragment MyUser on BackendUser {
    id
    salutation
    title
    firstName
    lastName
    gender
    birthdate
    street
    houseNr
    postalCode
    city
    country
    phone
    email
    lastOnline
    deactivationTimestamp
}

And regarding parsing, I also tried to parse json with:

val jsonReader = BufferedSourceJsonReader(source = Buffer().write(mockResponseGetUser.encodeUtf8()))
val user = MyUserImpl_ResponseAdapter.MyUser.fromJson(jsonReader, CustomScalarAdapters.Empty)

And mockResponseGetUser had the content of the body.

In that case I got exception

Expected a name but was BEGIN_OBJECT at path 
com.apollographql.apollo3.exception.JsonDataException: Expected a name but was BEGIN_OBJECT at path 
	at app//com.apollographql.apollo3.api.json.BufferedSourceJsonReader.nextName(BufferedSourceJsonReader.kt:407)
	at app//com.apollographql.apollo3.api.json.BufferedSourceJsonReader.selectName(BufferedSourceJsonReader.kt:707)
	at app//com.sth.backend.api.apollo.codegen.fragment.MyUserImpl_ResponseAdapter$MyUser.fromJson(MyUserImpl_ResponseAdapter.kt:76)
	at app//com.sth.mobile.requests.ApolloExtensionsTest$Success unwrap apollo response$1.invokeSuspend(ApolloExtensionsTest.kt:72)
	at app//com.sth.mobile.requests.ApolloExtensionsTest$Success unwrap apollo response$1.invoke(ApolloExtensionsTest.kt)
	at app//com.sth.requests.ApolloExtensionsTest$Success unwrap apollo response$1.invoke(ApolloExtensionsTest.kt)
.....
1 test completed, 1 failed
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':shared:testDebugUnitTest'.
> There were failing tests. See the report at: file:///.../shared/build/reports/tests/testDebugUnitTest/index.html
* Try:
> Run with --scan to get full insights.
BUILD FAILED in 788ms

I use the apollo library within a kotlin multiplatform project.

@martinbonnin
Copy link
Contributor

val user = MyUserImpl_ResponseAdapter.MyUser.fromJson(jsonReader, CustomScalarAdapters.Empty)

This is internal APIs, you're not really supposed to call them directly. If you want, you'll need to call .obj():

-val user = MyUserImpl_ResponseAdapter.MyUser.fromJson(jsonReader, CustomScalarAdapters.Empty)
+val user = MyUserImpl_ResponseAdapter.MyUser.obj().fromJson(jsonReader, CustomScalarAdapters.Empty)

The public API to do the same thing is GetUserQuery().parseResponse(source). It also allows passing in variables and other information required to parse @include and @defer correctly

Back to your original problem, can you try adding __typename to your JSON?

{
                "data": {
                    "getUser": {
                        "__typename": "BackendUser",
                        "birthdate": "1990-11-08T00:00:00",
                        "city": null,
                        "country": null,
                        "deactivationTimestamp": null,
                        "email": "roxana@company.de",
                        "firstName": "Roxana",
                        "gender": "FEMALE",
                        "houseNr": null,
                        "id": "xxxx",
                        "lastName": "Test",
                        "phone": null,
                        "postalCode": null,
                        "salutation": "MRS",
                        "street": null,
                        "title": null,
                        "lastOnline": "2024-01-25T18:18:52"
                    }
                }
              }

The Apollo Compiler queries __typename automatically for polymorphism and cache reasons. If it doesn't find it, it will fail the parsing as well (and you should get an exception in response.exception)

@martinbonnin
Copy link
Contributor

PS: Note that the exception is not thrown, it's part of response.exception. This is a change in v4. See this issue for more details.

@rb090
Copy link
Author

rb090 commented Jan 26, 2024

@martinbonnin thank you so so much ❤️. Ya it was mit good old friend __typename 😂🙈. Now, when setting __typename within the json like you proposed, the MockResponse gets enqueued perfectly. Thank you for your patience and your explanations. That helps really a lot.

Regarding the obj().fromJson:

This is internal APIs, you're not really supposed to call them directly. If you want, you'll need to call .obj():

-val user = MyUserImpl_ResponseAdapter.MyUser.fromJson(jsonReader, CustomScalarAdapters.Empty)
+val user = MyUserImpl_ResponseAdapter.MyUser.obj().fromJson(jsonReader, CustomScalarAdapters.Empty)

Ah okay, thank you for clarification that this is internal and should not be used from outside. I now used GetUserQuery().parseResponse(source) and that worked like a charm.

val user = MyUserImpl_ResponseAdapter.MyUser.obj().fromJson(jsonReader, CustomScalarAdapters.Empty) call with .obj(). still ends up in error:

Field 'id' is missing or null at path [null]
com.apollographql.apollo3.exception.DefaultApolloException: Field 'id' is missing or null at path [null]
	at app//com.apollographql.apollo3.api.Assertions__AssertionsKt.missingField(Assertions.kt:38)
	at app//com.apollographql.apollo3.api.Assertions.missingField(Unknown Source)
	at app//com.backend.api.apollo.codegen.fragmentMyUserImpl_ResponseAdapter$MyUser.fromJson(MyUserImpl_ResponseAdapter.kt:110)
	at app//com.backend.api.apollo.codegen.fragment.MyUserImpl_ResponseAdapter$MyUser.fromJson(MyUserImpl_ResponseAdapter.kt:35)
	at app//com.apollographql.apollo3.api.ObjectAdapter.fromJson(Adapters.kt:359)
	at app//com.myapp.mobile.requests.ApolloExtensionsTest$Success unwrap apollo response$1.invokeSuspend(ApolloExtensionsTest.kt:74)
	at app//com.myapp.mobile.requests.ApolloExtensionsTest$Success unwrap apollo response$1.invoke(ApolloExtensionsTest.kt)
	at app//com.myapp.mobile.requests.ApolloExtensionsTest$Success unwrap apollo response$1.invoke(ApolloExtensionsTest.kt)
	at app//kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invokeSuspend(TestBuilders.kt:319)
	at app//kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at app//kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
	at app//kotlinx.coroutines.test.TestDispatcher.processEvent$kotlinx_coroutines_test(TestDispatcher.kt:28)
	at app//kotlinx.coroutines.test.TestCoroutineScheduler.tryRunNextTaskUnless$kotlinx_coroutines_test(TestCoroutineScheduler.kt:103)
...

I am not sure why, this is just FYI maybe there is an issue somewhere?

PS: Note that the exception is not thrown, it's part of response.exception. This is a change in v4. See #4711 for more details.

Aaaahh okay 👍, oh this is cool tbh 😍, so thank you for the hint here. From what I see now, there is exception in case of network failure fe.

Nonetheless, there is also ApolloException thrown on ApolloCall<D : Operation.Data>.execute(). But this exception is called when:

if the call returns zero or multiple valid GraphQL responses.

Do I understand it correctly that this is thrown in case ApolloResponse.data and ApolloResponse.errors and ApolloResponse.exception are null? Or when there is an ApolloResponse.exception AND ApolloResponse.data?

@martinbonnin
Copy link
Contributor

Field 'id' is missing or null at path [null]

My bad, MyUserImpl_ResponseAdapter.MyUser is expecting a User (without data), not a response so if you're using that you need to pass a JSON that looks like { "id": ..., "firstName": ...}, etc...

But using parseResponse() is much better and will avoid those kind of issues.

execute(): ApolloException is thrown if the call returns zero or multiple valid GraphQL responses.

execute() throws if the call returns zero or multiple responses. When this happens this is typically a programming mistake. For an example, you used .execute() on a subscription (that returns multiple responses) or on a @defer query. In those cases, you need to call .toFlow().

For server/network errors, etc.. it's all in response.exception.

  • You can have data == null && errors == null && exception == null if a server is non-compliant and returns {}
  • It's impossible to have exception != null && data != null though. If you have data, it means a response was successfully returned and therefore exception is always null.

I typically recommend to handle errors like this:

if (response.data != null) {
  // Handle (potentially partial) data
} else {
  // Something wrong happened
  if (response.exception != null) {
    // Handle non-GraphQL errors (network, cache miss, etc...)
  } else {
    // Handle GraphQL errors in response.errors
  }
}

If you don't want to deal with partial data, you can opt-in the new error-aware parsing. You can read https://www.apollographql.com/docs/kotlin/v4/advanced/nullability#error-aware-parsing for more details.

Hope this helps!

@rb090
Copy link
Author

rb090 commented Jan 26, 2024

@martinbonnin thank you so much for the explanation and the informations. Now everything makes sense for me.

I really like the new Error aware parsing within apollo v4 😍 - very nice addition.

But do I understand it correctly, that when I specify throwing errors in case that parsing fails with extend schema @catch(to: THROW) it is for all queries and mutations?

And in case I want some queries and mutations to behave different from that "standard" and to display the data returned from the server, even if it is partial I would need to configure @catch for the single attributes where I am okay with the failing right?

@martinbonnin
Copy link
Contributor

martinbonnin commented Jan 26, 2024

And in case I want some queries and mutations to behave different from that "standard" and to display the data returned from the server, even if it is partial I would need to configure @catch for the single attributes where I am okay with the failing right?

That is correct. With extend schema @catch(to: THROW), the error handling becomes opt-in. You'll have to opt-in on individual fields with @catch(to: NULL) or @catch(to: RESULT) if needed.

@rb090
Copy link
Author

rb090 commented Jan 26, 2024

Ah okay. That make totally sense, now I got the whole idea on that and how it works. And I will start using it.

Thanks a lot for the explanations 🙌 ❤️. I guess now my questions are totally answers and I really learned again a lot about the Kotlin apollo client which is a lot more nicer since the last time I used it 🙂.

I wish you a wonderful weekend and close this issue.

@rb090 rb090 closed this as completed Jan 26, 2024
@martinbonnin
Copy link
Contributor

Many thanks for the kind words 💙😃. Let us know how the error-aware parsing works for you. This is brand new functionality and your feedback is super important!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants