mswjs / msw

Industry standard API mocking for JavaScript.
https://mswjs.io
MIT License
15.94k stars 518 forks source link

Improve GraphQL mocking #184

Closed sibelius closed 4 years ago

sibelius commented 4 years ago

Is your feature request related to a problem? Please describe. Right now you need to mock the whole GraphQL response like this https://github.com/mswjs/msw/blob/e31a344332f5951177bb17655a82c7cefe59445e/test/graphql-api/query.mocks.ts

Describe the solution you'd like The idea is to only mock the types and fields that you need in your test

Describe alternatives you've considered I've been using relay-testing-utils to mock only parts of my GraphQL response https://relay.dev/docs/en/testing-relay-components We can also use graphql-tools mocking approach (https://www.apollographql.com/docs/graphql-tools/mocking/)

Additional context This is based on a twitter discussion (https://twitter.com/sseraphini/status/1264877251587432448) about graphql-mock-api

brapifra commented 4 years ago

Hey @kettanaito! I would love to help you with this. I was initially experimenting with the idea of building something on the top of msw, but I think it makes sense to add it as part of the library. Also, it should be quite straightforward to implement!

Experiment ```javascript const { makeExecutableSchema, addMocksToSchema } = require('graphql-tools'); const { graphqlSync } = require('graphql'); const { graphql } = require('msw'); const { setupServer } = require('msw/node'); const { default: ApolloClient, gql } = require('apollo-boost'); const merge = require('lodash/merge'); const fetch = require('node-fetch'); // Creates a basic schema with mocks function getSchemaWithMocks() { const schemaString = ` type User { id: Int!, name: String! } type Query { user(id: Int): User! } `; const schema = makeExecutableSchema({ typeDefs: schemaString }); const schemaWithMocks = addMocksToSchema({ schema }); return schemaWithMocks; } const schemaWithMocks = getSchemaWithMocks(); // Setup server const server = setupServer( graphql.query('getUser', (req, res, ctx) => { const { data, errors } = graphqlSync(schemaWithMocks, req.body.query); if (errors) { return res(ctx.errors(errors)); } return res( ctx.data(merge(data, { user: { name: 'This is a custom name' } })) ); }) ); server.listen(); // Create graphql client const client = new ApolloClient({ uri: 'https://fake_domain.com/graphql', fetch, }); // Query user client .query({ query: gql` query getUser { user(id: 6) { id name } } `, }) .then((res) => console.log(res)) .catch((error) => console.log(error)); ```

I've also come up with a potential API that msw could provide:

Potential API ```javascript const { graphql } = require('msw'); const { setupServer } = require('msw/node'); const { default: ApolloClient, gql } = require('apollo-boost'); const fetch = require('node-fetch'); const { privateSchemaWithMocks, publicSchemaWithMocks, } = require('./schemasWithMocks'); const PRIVATE_SCHEMA_ENDPOINT = 'https://fake_domain.com/graphql'; const PUBLIC_SCHEMA_ENDPOINT = 'https://public_fake_domain.com/public/graphql'; // Setup server // The new graphql.schema handler returns a GraphQLSchema that will then // be used by msw to get an initial mocked response. // It's valuable to be able to return a different schema dynamically // for projects that use more than 1 graphql endpoint/schema const server = setupServer( graphql.schema((request) => { if (request.url.origin === PRIVATE_SCHEMA_ENDPOINT) { return privateSchemaWithMocks; } return publicSchemaWithMocks; }), graphql.query('getUser', (req, res, ctx) => { // The response here can be partial because msw is going to merge it // with the mocked response provided by the above schema return res( ctx.data({ user: { name: 'This is a custom name' } }) ); }) ); server.listen(); // Create private graphql client const privateClient = new ApolloClient({ uri: PRIVATE_SCHEMA_ENDPOINT, fetch, }); // This will be replied with the mocked data from privateSchemaWithMocks privateClient .query({ query: gql` query getUser { user(id: 6) { id name } } `, }) .then((res) => console.log(res)) .catch((error) => console.log(error)); // Create public graphql client const publicClient = new ApolloClient({ uri: PUBLIC_SCHEMA_ENDPOINT, fetch, }); // This will be replied with the mocked data from publicSchemaWithMocks publicClient .query({ query: gql` query getUser { user(id: 6) { id name } } `, }) .then((res) => console.log(res)) .catch((error) => console.log(error)); ```

Let me know what are your thoughts on this! And if you are happy, I'm keen on creating a PR for it!

kettanaito commented 4 years ago

Hey, @brapifra! Woah, that's a great proof of concept you've shared! Please, give me some time to get my head around it, I'll get back with the feedback on it.

Meanwhile, I can share a few concerns I have with extending a GraphQL API mocking support:

Type-based vs. Operation-based

Current GraphQL API mocking concept in MSW is operation based. This means that you create a declarative mapping between a GraphQL operation (query/mutation) and a mocked response. This inherits a similar approach of mocking REST API provided by the library.

While this is arguably the easiest way to mock a specific operation(s), I see a potential application of type-based mocking. This is exactly what you are doing in the examples above: reusing a mocked schema and feed it to a designated library's API.

Benefits

Concerns

Extends of an API

When I was tossing around the thoughts on how GraphQL types can be mocked, I settled on something like this:

import { graphql } from 'msw'

// Any GraphQL operation querying for the "User" type
// will receive this chunk of data as the value of that field.
// At this point, this is a GraphQL type resolver.
graphql.type('User', (req, res, ctx) => {
  return res(ctx.data({ firstName: faker.name(), age: faker.number() })
}),
// Can this concept be pushed to resolving primitive types
// with mocked values? Is this actually beneficial, or creates more problems that it solves?
graphql.type('String', (req, res, ctx) => res(ctx.data(faker.string()))

Benefits

Concerns

sibelius commented 4 years ago

operation based needs you to mock the whole operation, this is not practical when testing different scenarios

imagine I have a button that render differently based on a field called active inside my User type

Using type-based mocking I can do this

User: () => ({ active: true })

to mock when User is active, and do this

User: () => ({ active: false })

I don't care about all the rest of my operation for this test

brapifra commented 4 years ago

Great points! Here are my thoughts:

brapifra commented 4 years ago

Any updates on this? Still thinking about it @kettanaito? :smile:

kettanaito commented 4 years ago

Didn't have time to look into this issue.

I believe we should start from adding a support for schema-based mocking. Something like graphql.schema() request handler.

Partial response. Name is generated by msw

MSW shouldn't generate any data, it's an additional responsibility and one should use designated libraries to do that. MSW should allow to capture a request if it matches a predicate, and respond with an arbitrary mocked response. This leaves the developer to decide what to generate or mock.

In GraphQL's context, if you don't provide a resolver for some type, you don't expect it to be mocked, just as you don't expect that resolver to work in an actual GraphQL server.

The focus now is to come up and discuss a possible API for the graphql.schema() request handler. Do you have any ideas on how you see this API working?

brapifra commented 4 years ago

Yup, I agree. The way I see this graphql.schema API is as shown in my previous comment https://github.com/mswjs/msw/issues/184#issuecomment-646761262 Basically:

// Setup server
// The new graphql.schema handler returns a GraphQLSchema that will then
// be used by msw to get an initial mocked response.
// It's valuable to be able to return a different schema dynamically
// for projects that use more than 1 graphql endpoint/schema
const server = setupServer(
  graphql.schema(request => {
    if (request.url.origin === PRIVATE_SCHEMA_ENDPOINT) {
      return privateSchemaWithMocks;
    }
    return publicSchemaWithMocks;
  }),
  graphql.query('getUser', (req, res, ctx) => {
    // The response here can be partial because msw is going to merge it
    // with the mocked response provided by the above schema
    return res(
      ctx.data({ user: { name: 'This is a custom name' } })
    );
  })
);
server.listen();
kettanaito commented 4 years ago

The response here can be partial because msw is going to merge it with the mocked response provided by the above schema

Not sure about this. Currently there is no concept in MSW to persist state between request handlers. This would make mocking logic less readable, and dependent tests less reliable. Overall, despite request handlers bearing asynchronous nature and potentially having external dependencies, you should strive towards treating request handlers as pure functions. Request handler accepts a request and may return a response. We shouldn't complicate that.

It comes to the expectations about how graphql.schema() would work. Also, how would it work in a combination with other GraphQL request handlers, such as graphql.query? What if I perform this query:

query GetPost {
  post {
    title
    category
  }
}

And have these request handlers:

graphql.schema((req) => {
  return {
    Post: {
      category: () => 'Education'
    }
  }
}),
graphql.query('GetPost', (req, res, ctx) => {
  // What if I provide an explicit category value here?
  // What if I don't provide any category here?
  // Keep in mind TypeScript support, which allows you to ensure your response resolver
  // returns _all_ the data you specified in your query.
  return res(ctx.data({ post: { title: 'Article', category: 'Programming' })
})

With the API you suggest graphql.schema is more of a configuration option, than a request handler.

brapifra commented 4 years ago

Good points!

kettanaito commented 4 years ago

It appears that what we are speaking about is either a config, or a context utility. There is no limit on complexity towards a context utility. Consider:

graphql.query('GetUser', (req, res, ctx) => {
  const mockSchema = req.url.searchParams.get('factor')
    ? devMockSchema
    : integMockSchema

  return res(
    ctx.withSchema(mockSchema),
    ctx.data({
      user: {
        firstName: 'John'
      }
    })
  )
})

There are technical implications to implement such context utility, but may be worth considering. What do you think about this?

brapifra commented 4 years ago

Yes! I was literally thinking on something like this now. I just miss one last detail: The ability to have some kind of wildcard like

// I setup the server with this to automatically mock any query
graphql.anyQuery((req, res, ctx) => res(ctx.withSchema(mockSchema));
graphql.anyMutation((req, res, ctx) => res(ctx.withSchema(mockSchema));

// Runtime mocks still overwrite the above mock
graphql.query('GetUser', (req, res, ctx) => {
  const mockSchema = req.url.searchParams.get('factor')
    ? devMockSchema
    : integMockSchema

  return res(
    ctx.withSchema(mockSchema),
    ctx.data({
      user: {
        firstName: 'John'
      }
    })
  )
})

That way, graphql operations that are not relevant to a test, can be automatically replied by anyQuery/anyMutation.

garhbod commented 4 years ago

Love the progress on this topic. Just on the last comment could the api handle the wildcard as it does in rest and instead of graphql.anyQuery((req, res, ctx) => {...}) it could be graphql.query('*', (req, res, ctx) => {...})

kettanaito commented 4 years ago

Building a fallback hierarchy of handlers that specify both schema- and operation-based handlers is complicated. I'd love us to give it a try, though. I believe there's been enough discussion to kick off a proof of concept implementation and I'm looking for a brave contributor(s) to help me in this.

API

Let's start by adding a new graphql.schema() request handler.

Call signature

type GraphQLSchema = (endpoint: string, schema: unknown) => RequestHandler

We should start approaching GraphQL mocking with custom endpoints in mind, to support use case of a single client communicating with multiple GraphQL servers.

Usage example

const { setupWorker, graphql } from 'msw'

const worker = setupWorker(
  graphl.schema('http://localhost:8080/graphql', graphqlSchema)
)

worker.start()

Expectations

Implementation

We can start by matching such GraphQL operation and attempting to resolve it using the graphql-js package:

import { graphql } from 'graphql'

const abstractSchemaMethod = (operation) => {
  return graphql(schema, operation)
}

If you would like to contribute this, please comment under this post so others could see and collaborate with you. Please start any contribution from an integration test. Thank you!

brapifra commented 4 years ago

Hey @kettanaito, as I mentioned before, I'm happy to contribute :smile: A couple of questions:

We should start approaching GraphQL mocking with custom endpoints in mind, to support use case of a single client communicating with multiple GraphQL servers.

Isn't this something that could be valuable for REST mocking too?

Also, I assume that the ctx.withSchema util fn is something you would like to add later on? I think most people are more interested in that (partial mocking of responses) than in full-mocking.

garhbod commented 4 years ago

Just so I am understanding correctly the intended functionality of graphl.schema(endpoint, schema) is that it automatically populates the mock response?

So a graphql.schema(...) like below

graphql.schema(
    '/graphql',
    `
        type Query {
            hero: Character
        }

        type Character {
            name: String
            friends: [Character]
            homeWorld: Planet
            species: Species
        }

        type Planet {
            name: String
            climate: String
        }

        type Species {
            name: String
            lifespan: Int
            origin: Planet
        }
    `
)

With a request like below

query hero {
    hero {
        name
        homeWorld {
            name
        }
    }
}

Would automatically respond with something like the below?

{
    "data": {
        "hero": {
            "name": "Random String",
            "homeWorld": {
                "name": "Other Random String",
            }
        }
    }
}
garhbod commented 4 years ago

If that is the intended functionality/purpose then isn't that beyond the scope of this package? I feel like the catch all graphql.query('*', (req, res, ctx) => {...}) would be a better avenue and use a third party to do the mocking.

Example https://www.graphql-tools.com/docs/mocking/

brapifra commented 4 years ago

Yeah, maybe msw should simply add the ability to enrich any response. And then auto-mocking could be added on the top of that. It could work similarly to https://mswjs.io/docs/basics/response-transformer#example

function enrichGraphqlResponse(res) {
    const mockDataFromSchema = ...;
    res.data = merge(res.data, mockDataFromSchema);
    return res;
}

graphql.query('getUser', (req, res, ctx) => res(ctx.data({ user: { name: 'This is a name' }})));

graphql.query('*', (req, res, ctx) => res(enrichGraphqlResponse)); // This is run after `graphql.query('getUser', ...)` gets executed
kettanaito commented 4 years ago

@brapifra, I think such response enhancement can quickly grow out of hand. Imagine if you have one set of request handlers in one part of your setup, and another in the other part. If you know that response may be modified after it's responded with, you are never sure what would the end response be. Generally, it's not a good practice, and it's also not how responses work in browser/Node. I'd treat res() as the actual response you make, meaning a response you can no longer modify, once used.

Instead of achieving such enhancement by complicating the library's API, consider creating custom response resolvers or request handlers, and embed that logic into them. For example:

import { response } from 'msw'

function graphqlResponse(...transformers) {
  const mockedResponse = response(...transformers)
  return modify(mockedResponse)
}

graphql.query('GetUser', (req, res, ctx) => graphqlResponse(ctx.data({ ... })))

This way your handlers are easier to read and give you assurance the response you see is the response you get. Functional composition is much more versatile, and we are actively encouraging experimenting with response resolvers to create the functionality you need. Once we spot common practices, we can discuss about adding them to the library's API, but I'm afraid mutating mocked response once it's been used won't be the case.

kettanaito commented 4 years ago

Update: we are adding per-endpoint GraphQL operations matching in #319.

@brapifra, that should be a good underlying basis to get you started :)

kettanaito commented 4 years ago

@garhbod, I think you understood it correctly. graphql.schema would resolve a captured GraphQL operation against a given schema, just like GraphQL itself does. Capturing all GraphQL requests and performing a mocking using third-party tool sounds sensible as well. At least, there should be such option, if developer decides so.

Does this mean we should introduce something like graphql.operation(resolver)? A request handler that would capture all GraphQL operations (query and mutations) and apply given response resolver to them. That way we could do:

graphql.operation((req, res, ctx) => {
  const data = thirdPartyTool(req.body.query)
  return res(ctx.data(data))
})

With such API we can build a native response resolver as a part of library's API:

const withSchema = (schema) => {
  return (req, res, ctx) => {...}
}

graphql.operation(withSchema(mySchema)))

There should be packages that would resolve queries against a given schema, so you don't have to configure it. Would such API be too opinionated? Where would the mocked data come from for such API?

garhbod commented 4 years ago

I think the .operation(...) is all that is required and then use it with third party schema mocking. I've created a working example with just queries and used a generic QueryType of catchAll to make a pseudo operations function.

It is the browser integration so simple npm install and npm run server to see it in action https://github.com/garhbod/msw-graphql-example

kettanaito commented 4 years ago

@garhbod, sounds great! Would you be interested in creating a pull request to this repo? We'd iterate on that API and refine it together. Let me know.

garhbod commented 4 years ago

Yeah sounds good. I haven't done much TypeScript so be prepared to refactor lol

brapifra commented 4 years ago

Agreed. I think the graphql.operation could pretty much cover all cases mentioned here. I see that @garhbod is planning to contribute with a PR, great! I'm happy to help if needed!

kettanaito commented 4 years ago

We've shipped support for graphql.operation request handler in msw@0.21.0. Updating to that version should allow you to use that request handler to resolve any GraphQL operation any way you wish (i.e. using an external server).

I'll close this issue, concluding its scope being graphql.operation support. That is to prevent it from being forever open without a clear milestone. We still have a way to go with enhancing GraphQL support with adding subscriptions and providing other improvements to the experience. Please don't hesitate to reach out and share your feedback on the GraphQL usage of MSW. You're awesome.