Closed sibelius closed 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!
I've also come up with a potential API that msw could provide:
Let me know what are your thoughts on this! And if you are happy, I'm keen on creating a PR for it!
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:
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.
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()))
graphql
wouldn't be able to resolve standalone). This may be an edge case, but I still think that involving as less graphql
as possible is a good thing.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
Great points! Here are my thoughts:
it may resemble to schema development rather than mock definition
, I think it shouldn't be a problem since devs could still not pass a mocked schema if they didn't want to, right? The way I see this is that it can provide a nice base of mocks, and then the dev only mocks what it's needed for the test/feature (pretty much in line with @sibelius' comment)
In addition, I believe in most cases devs won't be actually implementing the schema just to pass it to the library. They will probably be downloading it from the graphql endpoint and passing it to msw (That's actually the use case I have).const query = `
query getData {
user {
name
age
}
}
`;
graphql.query('getData', (req, res, ctx) => {
return res(ctx.data({ user: { age: 0 }})); // Partial response. Name is generated by msw
});
Any updates on this? Still thinking about it @kettanaito? :smile:
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?
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();
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.
Good points!
With the API you suggest graphql.schema is more of a configuration option, than a request handler.
It is indeed a configuration option, but the way I see it is that this config option can change depending on the request (e.g if the request is to endpoint X use schema Y...). That's why I was suggesting an API like the one above.
Nevertheless, I do agree that it could get confusing for some people.graphql.query/mutation
should overwrite the ones set in graphql.schema
graphql.query('GetPost', (req, res, ctx) => {
// The field "category" is gotten from schemaWithMocks
return res(ctx.partialData({ post: { title: 'Article' }}, schemaWithMocks)
})
graphql.query('GetPost', (req, res, ctx) => {
// The field "category" is 'Programming'. The one from schemaWithMocks is ignored
return res(ctx.partialData({ post: { title: 'Article', category: 'Programming' }}, schemaWithMocks))
})
It could get a bit annoying to always have to pass a variable like schemaWithMocks
, but at least the handlers would still be pure.
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?
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
.
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) => {...})
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.
Let's start by adding a new graphql.schema()
request handler.
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.
const { setupWorker, graphql } from 'msw'
const worker = setupWorker(
graphl.schema('http://localhost:8080/graphql', graphqlSchema)
)
worker.start()
GET
with query parameter and POST
with body for GraphQL operation request (see current implementation for reference).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!
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.
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",
}
}
}
}
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.
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
@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.
Update: we are adding per-endpoint GraphQL operations matching in #319.
@brapifra, that should be a good underlying basis to get you started :)
@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?
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
@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.
Yeah sounds good. I haven't done much TypeScript so be prepared to refactor lol
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!
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.
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