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

Trying to use MockMvc to test a GraphQL API #779

Closed
ivangfr opened this issue Aug 20, 2023 · 13 comments
Closed

Trying to use MockMvc to test a GraphQL API #779

ivangfr opened this issue Aug 20, 2023 · 13 comments
Labels
in: test Issues related to the test module status: declined A suggestion or change that we don't feel we should currently apply

Comments

@ivangfr
Copy link

ivangfr commented Aug 20, 2023

Hi, I have a Spring Boot app that exposes a GraphQL API, and it's secured using Okta. The app works fine.

Now, I am trying to implement some test cases. Instead of testing directly in an Okta endpoint, I was trying to use MockMvc. Also, I'd like to use the MockMvcRequestBuilders.jwt() to create easily different JWT to test the Queries and Mutations.

I've tried this setup

@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(properties = {
        "okta.oauth2.issuer=https://okta.okta.com/oauth2/default",
        "okta.oauth2.clientId=my-client-id"
})
class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testCreateBookWithAdminToken() throws Exception {
        ResultActions resultActions = mockMvc.perform(
                        post("/graphql")
                                .content(CREATE_BOOK_QUERY)
                                .contentType(MediaType.APPLICATION_GRAPHQL_RESPONSE)
                                .accept(MediaType.APPLICATION_GRAPHQL_RESPONSE)
                                .with(jwt()
                                        .authorities(new SimpleGrantedAuthority("BOOK-API-ADMIN"))
                                        .jwt(jwt -> jwt.claim(JwtClaimNames.SUB, "app-admin-test"))
                                ))
                .andDo(print());

        resultActions.andExpect(status().isOk());
    }
    ...

    private static final String CREATE_BOOK_QUERY = """
        mutation {
          createBook(bookInput: {title: "Java 17", author: "Peter", pages: 120}) {
            id
          }
        }
        """;
}

However, I am getting 404

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /graphql
       Parameters = {}
          Headers = [Content-Type:"application/graphql-response+json;charset=UTF-8", Accept:"application/graphql-response+json", Content-Length:"99"]
             Body = mutation {
  createBook(bookInput: {title: "Java 17", author: "Peter", pages: 120}) {
    id
  }
}

    Session Attrs = {}

Handler:
             Type = org.springframework.web.servlet.resource.ResourceHttpRequestHandler

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 404
    Error message = null
          Headers = [Vary:"Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers", X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

java.lang.AssertionError: Status expected:<200> but was:<404>
Expected :200
Actual   :404

I am missing something?

Best regards,

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 20, 2023
@hantsy
Copy link
Contributor

hantsy commented Aug 21, 2023

According to GraphQL HTTP protocol, the request body should be somelike.

{
  "query":"", 
  "variables": {}
}

@ivangfr
Copy link
Author

ivangfr commented Aug 21, 2023

Hi @hantsy Thanks for the response.

Indeed, I've changed my CREATE_BOOK_QUERY to

    private static final String CREATE_BOOK_QUERY = """
            {
              "query": "mutation { createBook(bookInput: {title: $arg1, author: $arg2, pages: $arg3}) { id } }",
              "operationName": "createBook",
              "variables": { "arg1": "Java 17", "arg2": "Peter", "arg3": 120}
            }
            """;

And now I am getting 200 SUCCESS.

Besides this change, I also needed to set .contentType(MediaType.APPLICATION_JSON)

Here is the implementation of the createBook Mutation

    @Secured("BOOK-API-ADMIN")
    @MutationMapping
    public Book createBook(@Argument BookInput bookInput) {
        return bookService.addBook(bookInput.title(), bookInput.author(), bookInput.pages());
    }

As we can see, it's secured and requires the authority "BOOK-API-ADMIN".

In my test case, I am building a request with a JWT that contains this authority

    @Test
    void testCreateBookWithAdminToken() throws Exception {
        ResultActions resultActions = mockMvc.perform(
                        post("/graphql")
                                .content(CREATE_BOOK_QUERY)
                                .contentType(MediaType.APPLICATION_JSON)
                                .with(jwt()
                                        .authorities(new SimpleGrantedAuthority("BOOK-API-ADMIN"))
                                        .jwt(jwt -> jwt.claim(JwtClaimNames.SUB, "app-admin-test"))
                                ))
                .andDo(print());

        resultActions.andExpect(status().isOk());
    }

However, if I do not inform the JWT, it should return me UNAUTHORIZED. Furthermore, if I inform a JWT without the "BOOK-API-ADMIN" authority, it should return FORBIDDEN.

I've tried those cases and, I am always getting 200 SUCCESS.

Do you know what it can be?

Thanks

@hantsy
Copy link
Contributor

hantsy commented Aug 21, 2023

Do you check the response body, is there an errors in the JSON node?

@ivangfr
Copy link
Author

ivangfr commented Aug 21, 2023

Sorry for not checking that.

The response body is empty.

MockHttpServletRequest:
      HTTP Method = POST
      Request URI = /graphql
       Parameters = {}
          Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"204"]
             Body = {
  "query": "mutation { createBook(bookInput: {title: $arg1, author: $arg2, pages: $arg3}) { id } }",
  "operationName": "createBook",
  "variables": { "arg1": "Java 17", "arg2": "Peter", "arg3": 120}
}

    Session Attrs = {}

Handler:
             Type = org.springframework.boot.autoconfigure.graphql.servlet.GraphQlWebMvcAutoConfiguration$$Lambda$2077/0x0000000135aa9628

Async:
    Async started = true
     Async result = org.springframework.web.servlet.function.DefaultEntityResponseBuilder$DefaultEntityResponse@2416378c

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

java.lang.AssertionError: Response content expected:<It must have some content> but was:<>
Expected :It must have some content
Actual   :

@bclozel
Copy link
Member

bclozel commented Aug 28, 2023

Spring GraphQL does not support MockMvc for testing.
You can choose to perform full integration tests (including with authentication) or use GraphQlTester for mock tests. Given GraphQL is transport-agnostic, tests involving transport-specific configuration should be done in full integration tests.

@bclozel bclozel closed this as not planned Won't fix, can't repro, duplicate, stale Aug 28, 2023
@bclozel bclozel added status: declined A suggestion or change that we don't feel we should currently apply in: test Issues related to the test module and removed status: waiting-for-triage An issue we've not yet triaged labels Aug 28, 2023
@nilshartmann
Copy link
Contributor

Hi @bclozel ,

just one question:

You can choose to perform full integration tests (including with authentication)

How would I do that? Use MockMvc (but then without GraphQL-specific support)?

Thanks,
Nils

@hantsy
Copy link
Contributor

hantsy commented Jan 17, 2024

@nilshartmann I have an integration test example of using TestRestTemplate, check https://github.com/hantsy/spring-graphql-sample/blob/master/legacy/graphql-java-kickstart/src/test/java/com/example/demo/DemoApplicationTests.java#L75-L99 this tests is for uploading file, you can change it to general query, mutation operations.

@nilshartmann
Copy link
Contributor

Thanks, @hantsy . In my "graphql-kickstart times" I built something similiar, but I'd like to know if there are any problems or drawbacks when using MockMvc with Spring for GraphQL (beside the fact the one would be responsible to create a valid request payload, which GraphqlTester already does.)

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Jan 17, 2024

It's not only creating the request payload but also assertions on the response that GraphQlTester helps with.

Generally, in the documentation for GraphQlTester you'll see the range of available options divided in two categories. One lets you test over a transport through a client. The other is to test on the server side without a client and without a transport or with a mocked transport. For example the ExecutionGraphQlServiceTester lets you focus on GraphQL execution without involving a transport. WebGraphQlTester on the other hand lets you prepare an HTTP request without actually running on a live server, and in that sense is conceptually close to MockMvc.

Specifically for MockMvc, it may be possible to use HttpGraphQlClient with MockMvcWebTestClient but that would need to be set up with a WebApplicationContext since MockMvc doesn't support functional endpoints in standalone mode.

It would help to understand your goals for testing rather than the outcome to use MockMvc. I should think that one of the available options would help, but if you could please clarify.

@hantsy
Copy link
Contributor

hantsy commented Jan 17, 2024

It is better to let HttpGraphQlClient(via a Builder) to accept an existing client as http client engine, such as RestClient, MockMvc, RestTemplate/TestRestTemplate, WebClient/WebTestClient.

@nilshartmann
Copy link
Contributor

Hi @rstoyanchev , thanks a lot for your reply!

I was just asking because in another project I'm using a REST API and thus testing heavily with MockMvc. Now with GraphQL, I have to use another testing API. While this is not a big deal (and actually the test features of HttpGraphQlTester are great) it has some (minor) drawbacks. One has to learn another testing api (esp. result matcher and that is my biggest pain point) and one cannot use the "extensions" that are available for MockMvc (in my case for example jwt PostRequestProcessor or also some self-written PostRequestProcessors).

Please don't get me wrong. This should not be a "feature request" for a MockMvc-based GraphQL testing API, as I understand there are lot more situations that you would have to support than me in my concrete project (that doesn't have functional endpoints for example). I just try to understand what it would mean to use MockMvc or if there are some hidden gotchas that one could run into. In my (naive) imagination, I had to create the request object (with query, variables, etc), submit it with a POST request and could then inspect the (json) result with the regular jsonPath ResultMatcher. On the other hand of course I would loose all graphql specific features from HttpGraphQlTester, like loading a document from the classpath or extracting the errors from a response.

@rstoyanchev
Copy link
Contributor

rstoyanchev commented Jan 17, 2024

@nilshartmann thanks for the additional context.

You can use MockMvc if you want, and in the beginning we also wondered whether we need a separate test API for GraphQL. After all it's just a JSON request and response in a specific format. We could have left it at that but whatever "little" work is required to do that still adds a lot of noise and makes tests more difficult to read.

What we've done instead is to create a dedicated API for GraphQL that also helps with swapping out the underlying transport as needed, or not having a transport at all. That allows tests to be at the level of the GraphQL protocol, rather than the underlying transport.

So the underlying transport should be a detail, and we do allow using MockMvc. Boot's AutoConfigureHttpGraphQlTester brings in MockMvc via WebTestClient as I mentioned earlier. There is a code snippet at the end of this testing section of the Boot reference that shows using it. Note also that because of the pluggable transport, GraphQlTester and GraphQlClient don't expose transport level details (like headers) to change per request, and remain agnostic. However, you can mutate the client and set headers in each test.

@nilshartmann
Copy link
Contributor

@rstoyanchev thanks for your clarification. Helps a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: test Issues related to the test module status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

6 participants