Closed thawsitt closed 4 years ago
Just for debugging purposes, I added this in my Jest unit test file. (I only do this once. I comment this out in other runs)
const fn: any = (url: any, config: any) => {
console.warn(url);
console.warn(config);
};
jest.spyOn(window, 'fetch').mockImplementation(fn);
Here is what it prints out in terminal when I run the test.
http://localhost:3000/graphql
{
method: 'POST',
headers: { accept: '*/*', 'content-type': 'application/json' },
credentials: undefined,
signal: AbortSignal {},
body: '{"operationName":"GetSourceDetail","variables":{"ids":["f3701660-1c06-4ff5-93e0-5ac772581553","0cab5f37-6050-4009-94ac-40509a40c330"]},"query":"query GetSourceDetail($ids: [String!]!) {\\n getSourceDetailById(ids: $ids, type: LOGICAL_TABLE) {\\n id\\n name\\n columns {\\n name\\n id\\n dataType\\n modified\\n __typename\\n }\\n __typename\\n }\\n}\\n"}'
}
My handlers.js
file
// graphql-handlers.js
import { graphql } from 'msw';
export const handlers = [
graphql.query('GetSourceDetail', (req, res, ctx) => {
// Note: This does not get printed. (i.e. the query doesn't get intercepted)
console.warn('Intercepting graphql query', req.variables);
return res(
ctx.data({
data: [
{ ... someMockData}
],
error: null,
loading: false,
}),
);
}),
];
Jest setup file
// jest/jest-graphql-mock-server.js
import { setupServer } from 'msw/node';
import { handlers } from './graphql-handlers';
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
In jest.config.js
// jest.config.js
module.exports = {
// ... other settings,
setupFilesAfterEnv: ['./jest/jest-graphql-mock-server.js'],
};
Hey, @thawsitt. Thanks for raising this.
- Do I need any extra setup to make
msw
work in node (Jest unit tests)?
No. Calling setupServer
either in a test suite directly, or as a part of test teardown is enough to enable requests interception in a NodeJS process.
- Is there any config needed for different url end point?
Mock Service Worker intercepts GraphQL operations based on operation kind. Hostnames and endpoints are ignored during the request matching process. So you don't have to configure where your GraphQL server endpoint is.
I think to understand this issue better, let's analyze how MSW matches GraphQL requests and try to spot what may go wrong in your case.
Whenever any request happens, and you have a graphql.*
request handler attached, that handler parses a captured request to determine:
query
, either in the search params of the URL (in case of a GET
request), or in req.body.query
(in case of a POST
request).query
is found, the library parses it using graphql
JavaScript implementation. Parsing the query allows to get more information about it: its name and variables, for example. I think this is where the issue may be, because I see your query string containing an external type reference LOGICAL_TABLE
. query
is successfully parsed, the library uses a mocked response you defined as a response for that GraphQL operation.My guess goes that graphql.parse()
fails due to the external type being referenced in a query. It'd be nice to verify that by modifying the original GraphQL example for its query to contain some external type as well.
Okay, I've tried using an external type in the GraphQL query of the example, and it passed:
const LOG_IN = gql`
mutation Login($username: String!, $type: INTERNAL_USER_TYPE) {
user {
id
firstName
lastName
}
}
`
The issue must be eslewhere.
@thawsitt, could you please post some more info about the test?
GetSourceDetail
GraphQL query is executed.DEBUG=* npm run your-test-command
? This will print out additional information about the interception. Please post it here.Ideally, it would be awesome to have a reproduction repo (you can base it on the Examples repo fork, modifying existing GraphQL usage example). That way I could debug the issue much faster. Thanks.
Also worth noticing: ensure your GraphQL query is a valid query. Otherwise, the library will fail in parsing it (using the graphql
package), and that request will not be intercepted.
I've tried performing this query:
fetch('/graphql', {
method: 'POST',
headers: {
accept: '*/*',
'content-type': 'application/json'
},
credentials: undefined,
body: '{"operationName":"GetSourceDetail","variables":{"ids":["f3701660-1c06-4ff5-93e0-5ac772581553","0cab5f37-6050-4009-94ac-40509a40c330"]},"query":"query GetSourceDetail($ids: [String!]!) {\\n getSourceDetailById(ids: $ids, type: LOGICAL_TABLE) {\\n id\\n name\\n columns {\\n name\\n id\\n dataType\\n modified\\n __typename\\n }\\n __typename\\n }\\n}\\n"}'
})
Using this setup:
import { setupWorker, graphql } from 'msw'
const worker = setupWorker(
graphql.query('GetSourceDetail', (req, res, ctx) => {
return res(ctx.data({ id: 12 }))
}),
)
worker.start()
And see it being intercepted and mocked in a browser (using setupWorker
):
Let me see how this behaves in NodeJS.
I've tried performing that query in a Jest test using the following test suite:
/**
* @jest-environment node
*/
import { setupServer } from 'msw/node'
import { graphql } from 'msw'
import fetch from 'node-fetch'
const server = setupServer(
graphql.query('GetSourceDetail', (req, res, ctx) => {
return res(ctx.data({ id: 12 }))
}),
)
server.listen()
test('works', async () => {
const res = await fetch('http://localhost:3000/graphql', {
method: 'POST',
headers: {
accept: '*/*',
'content-type': 'application/json',
},
body:
'{"operationName":"GetSourceDetail","variables":{"ids":["f3701660-1c06-4ff5-93e0-5ac772581553","0cab5f37-6050-4009-94ac-40509a40c330"]},"query":"query GetSourceDetail($ids: [String!]!) {\\n getSourceDetailById(ids: $ids, type: LOGICAL_TABLE) {\\n id\\n name\\n columns {\\n name\\n id\\n dataType\\n modified\\n __typename\\n }\\n __typename\\n }\\n}\\n"}',
})
const json = await res.json()
expect(json).toEqual({ data: { id: 12 } })
})
This test passes, and the GraphQL query gets intercepted and mocked.
A couple of things I'd suggest:
npm install msw@latest
.setupFiles
in your jest.config.js
instead of https://jestjs.io/docs/en/configuration#setupfilesafterenv-array
. I see there's a difference of where those modules are executed. This may also be the reason interception fails for you.Hello. I am also running into this problem where GraphQL queries are not intercepted in Jest.
I have this line in my setupTests.js
:
global.fetch = require('jest-fetch-mock');
that is needed to instantiate a BatchHttpLink
from apollo-link-batch-http
that gets passed into ApolloClient
.
Just wondering if setting that fetch value in setupTests.js
has anything to do with this issue.
Hey, @Penspinner. I don't think that using any kind of fetch mock/polyfill would affect requests interception. Any polyfill uses either a native http
/https
module, its abstraction (i.e. request
), or an underlying polyfill (like whatwg-fetch
uses XMLHttpRequest
polyfilled by jsdom).
Please, if you have a reproduction repository, could you push it so we could take a look at it? There's definitely some factor at place, but we can find it once we are close to the context you are in. Thanks.
Does anyone know how to wait for results, before, when I was using MockedProvider
I used to do this:
jest.useFakeTimers()
...
render(
<MockedProvider mocks={mocks}>
<App />
</MockedProvider>
);
expect(screen.getByText("Loading")).toBeInTheDocument();
act(() => {
jest.runAllTimers()
})
expect(
screen.getByText(`${country.name} - ${country.code}`)
).toBeInTheDocument();
but now I don't know how to change the state, because the app just keeps in a loading state.
If I understood your case, before using MSW you used to mock your API using something like setTimeout, now using MSW you don't know when loading is finished right ?
If I understood your case, before using MSW you used to mock your API using something like setTimeout, now using MSW you don't know when loading is finished right ?
Yes, I was using runAllTimers to wait for the response.
The documentation of Apollo uses wait
but that didn't use to work for me, so I started using jest.runAllTimers()
to wait for the response to finish.
// This snippet is from Apollo and `wait` on its own didn't use to work for me
const wait = require('waait');
it('should render dog', async () => {
const dogMock = {
request: {
query: GET_DOG_QUERY,
variables: { name: 'Buck' },
},
result: {
data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } },
},
};
const component = renderer.create(
<MockedProvider mocks={[dogMock]} addTypename={false}>
<Dog name="Buck" />
</MockedProvider>,
);
await wait(0); // wait for response
const p = component.root.findByType('p');
expect(p.children).toContain('Buck is a poodle');
});
Fortunately, I was able to solve it. Playing around, I got to this and it worked.
Before, using MockedProvider
expect(screen.getByText("Loading")).toBeInTheDocument();
act(() => {
jest.runAllTimers() // wait for response
})
expect(
screen.getByText(`${country.name} - ${country.code}`)
).toBeInTheDocument();
using MSW
expect(screen.getByText("Loading")).toBeInTheDocument();
await act(async () => {
await wait(10); // wait for response
});
expect(
screen.getByText(`${country.name} - ${country.code}`)
).toBeInTheDocument();
ok, so you can do in this way
expect(screen.getByText("Loading")).toBeInTheDocument();
await waitForElementToBeRemoved(() => screen.getByText("Loading"));
expect(
screen.getByText(`${country.name} - ${country.code}`)
).toBeInTheDocument();
The reproduction wasn't reported for some time now, I'm treating this as a resolved then. Feel free to discuss and include any vital reproduction repositories so we could come back to this issue, if you still experiencing it. Thank you.
For anyone else following this issue and landing here, using jest-fetch-mock
in your setupTests.ts file can break MSW within a Node.js / Jest setting. I discovered this after a lot of trial and error today, and can't really identify why, but removing jest-fetch-mock fixed the problem for me here.
setupTest.ts file before:
import '@testing-library/jest-dom/extend-expect';
import { UIBreakpoints } from '@cx-labs/cisco-ui-react';
import { GlobalWithFetchMock } from 'jest-fetch-mock';
import { createMatchMedia } from './matchMedia.mock';
// Globally set default timeout to 30 seconds
jest.setTimeout(30000);
const customGlobal: GlobalWithFetchMock = global as GlobalWithFetchMock;
customGlobal.fetch = require('jest-fetch-mock');
customGlobal.fetchMock = customGlobal.fetch;
/**
* Default the UI Breakpoint to ExtraLarge, this can be overriden
* by reassigning window.matchMedia in an individual test
*/
window.matchMedia = createMatchMedia(UIBreakpoints.ExtraLarge);
/**
* Popper.js mock needed to fire React Bootstrap Dropdown events in JS DOM
* https://github.com/popperjs/popper.js/issues/478#issuecomment-341506071
*/
jest.mock('popper.js', () => {
const PopperJS = jest.requireActual('popper.js');
return class {
static placements = PopperJS.placements;
constructor() {
return {
destroy: () => {},
scheduleUpdate: () => {},
};
}
};
});
after (removed):
import '@testing-library/jest-dom/extend-expect';
import { UIBreakpoints } from '@cx-labs/cisco-ui-react';
import { createMatchMedia } from './matchMedia.mock';
// Globally set default timeout to 30 seconds
jest.setTimeout(30000);
/**
* Default the UI Breakpoint to ExtraLarge, this can be overriden
* by reassigning window.matchMedia in an individual test
*/
window.matchMedia = createMatchMedia(UIBreakpoints.ExtraLarge);
/**
* Popper.js mock needed to fire React Bootstrap Dropdown events in JS DOM
* https://github.com/popperjs/popper.js/issues/478#issuecomment-341506071
*/
jest.mock('popper.js', () => {
const PopperJS = jest.requireActual('popper.js');
return class {
static placements = PopperJS.placements;
constructor() {
return {
destroy: () => {},
scheduleUpdate: () => {},
};
}
};
});
That's a good comment. MSW is the replacement for jest-fetch-mock
. Using both doesn't guarantee any reliability and doesn't make much sense. Thanks for highlighting that.
For anyone coming to this in the future, I did have trouble intercepting requests made via Apollo's BatchHttpLink
(from apollo-link-batch-http
). Keeping everything else the same, using createHttpLink
(from apollo-link-http
), the requests were intercepted just fine.
One notable difference is that the request body
is normally an object, but BatchHttpLink
sends an array. @kettanaito Perhaps the library is not prepared to handle this body type?
MSW doesn’t currently support batched GraphQL queries. See the support progress in #513.
For me: the real problem was that I was using cross-fetch/polyfill in the jest setup :
import cross-fetch/polyfill
Deleting cross-fetch worked.
on the apollo client you still need to do this:
const httpLink = new HttpLink({
uri: '...',
fetch: window.fetch,
});
so if you are using anything to mock or support fetch in node js environment. DONT.
My current understanding is that MSW already does all of this stuff for you. so you really only need to tell apollo to use window.fetch, which MSW is intercepting, if you add cross-fetch or anything that is similarly related MSW will not get the request.
@dagadbm, to clarify, MSW doesn't provide any polyfills for you. Most of the issues arise from people forgetting to include a polyfill or doing so incorrectly.
In the context of Apollo, it depends on how you're setting up your tests. If you're using something like JSDOM, you need to consult Apollo's best practices on how to configure it on that testing environment. What I did in the past is: included a fetch polyfill, configured Apollo to always use window.fetch
(which in JSDOM will point to my polyfill).
But I dont have any polyfill and its working. Im so confused. I thought msw did this out of the box. How is this working then ?
@dagadbm, perhaps you're using a modern Node (17+) that ships with global fetch?
Node 16.19.0
Ok I am trying to run this with DEBUG=*
If I have this setup the graphQL requests get intercepted:
on apolloClient file:
const httpLink = new HttpLink({
uri: '/api/graphql',
fetch: window.fetch,
});
in jest.setup.js
console.log('fetch', window.fetch);
// import msw stuff
This is the important output:
console.log
fetch undefined
at Object.<anonymous> (client/app/test-utils/jest-setup.ts:4:9)
http constructing the interceptor... +0ms
xhr constructing the interceptor... +0ms
setup-server constructing the interceptor... +0ms
http:on adding "request" event listener: +0ms
async-event-emitter:on adding "request" listener... +0ms
xhr:on adding "request" event listener: +0ms
async-event-emitter:on adding "request" listener... +0ms
http:on adding "response" event listener: +0ms
async-event-emitter:on adding "response" listener... +0ms
xhr:on adding "response" event listener: +0ms
async-event-emitter:on adding "response" listener... +0ms
setup-server:apply applying the interceptor... +0ms
async-event-emitter:activate set state to: ACTIVE +0ms
setup-server:apply activated the emiter! ACTIVE +0ms
setup-server retrieved global instance: undefined +3s
setup-server:apply no running instance found, setting up a new instance... +0ms
setup-server:setup applying all 2 interceptors... +0ms
setup-server:setup applying "ClientRequestInterceptor" interceptor... +1ms
http:apply applying the interceptor... +0ms
async-event-emitter:activate set state to: ACTIVE +0ms
http:apply activated the emiter! ACTIVE +0ms
http retrieved global instance: undefined +3s
http:apply no running instance found, setting up a new instance... +0ms
http:setup native "http" module patched! +0ms
http:setup native "https" module patched! +0ms
http set global instance! http +0ms
setup-server:setup adding interceptor dispose subscription +0ms
setup-server:setup applying "XMLHttpRequestInterceptor" interceptor... +0ms
xhr:apply applying the interceptor... +0ms
async-event-emitter:activate set state to: ACTIVE +0ms
xhr:apply activated the emiter! ACTIVE +0ms
xhr retrieved global instance: undefined +3s
xhr:apply no running instance found, setting up a new instance... +0ms
xhr:setup patching "XMLHttpRequest" module... +0ms
xhr:setup native "XMLHttpRequest" module patched! XMLHttpRequestOverride +0ms
xhr set global instance! xhr +0ms
setup-server:setup adding interceptor dispose subscription +0ms
setup-server set global instance! setup-server +1ms
xhr:request POST /api/graphql open {
method: 'POST',
url: '/api/graphql',
async: true,
user: undefined,
password: undefined
} +0ms
xhr:request POST /api/graphql reset +1ms
xhr:request POST /api/graphql readyState change 0 -> 1 +0ms
xhr:request POST /api/graphql triggering readystate change... +0ms
xhr:request POST /api/graphql trigger "readystatechange" (1) +0ms
xhr:request POST /api/graphql resolve listener for event "readystatechange" +0ms
xhr:request POST /api/graphql set request header "accept" to "*/*" +0ms
xhr:request POST /api/graphql set request header "content-type" to "application/json" +0ms
xhr:request POST /api/graphql send POST /api/graphql +0ms
xhr:request POST /api/graphql request headers HeadersPolyfill {
[Symbol(normalizedHeaders)]: { accept: '*/*', 'content-type': 'application/json' },
[Symbol(rawHeaderNames)]: Map(2) { 'accept' => 'accept', 'content-type' => 'content-type' }
} +1ms
xhr:request POST /api/graphql emitting the "request" event for 1 listener(s)... +0ms
async-event-emitter:emit emitting "request" event... +0ms
async-event-emitter:openListenerQueue opening "request" listeners queue... +0ms
async-event-emitter:openListenerQueue no queue found, creating one... +0ms
async-event-emitter:emit appending a one-time cleanup "request" listener... +0ms
async-event-emitter:on adding "request" listener... +0ms
async-event-emitter:openListenerQueue opening "request" listeners queue... +0ms
async-event-emitter:openListenerQueue returning an exising queue: [] +0ms
async-event-emitter:on awaiting the "request" listener... +3s
async-event-emitter:openListenerQueue opening "request" listeners queue... +0ms
async-event-emitter:openListenerQueue returning an exising queue: [
{
args: [ [InteractiveIsomorphicRequest] ],
done: Promise { <pending> }
}
] +0ms
async-event-emitter:on awaiting the "request" listener... +1ms
xhr:request POST /api/graphql awaiting mocked response... +1ms
async-event-emitter:on "request" listener has resolved! +8ms
async-event-emitter:on "request" listener has resolved! +10ms
xhr:request POST /api/graphql all request listeners have been resolved! +9ms
xhr:request POST /api/graphql event.respondWith called with: {
status: 200,
statusText: 'OK',
headers: { 'x-powered-by': 'msw', 'content-type': 'application/json' },
// .... as you can see it is catching requests
Now if I do the recommended approach:
import 'cross-fetch/polyfill';
console.log('fetch', window.fetch);
This is the log:
console.log
fetch [Function: bound fetch] { polyfill: true }
at Object.<anonymous> (client/app/test-utils/jest-setup.ts:3:9)
http constructing the interceptor... +0ms
xhr constructing the interceptor... +0ms
setup-server constructing the interceptor... +0ms
http:on adding "request" event listener: +0ms
async-event-emitter:on adding "request" listener... +0ms
xhr:on adding "request" event listener: +0ms
async-event-emitter:on adding "request" listener... +0ms
http:on adding "response" event listener: +0ms
async-event-emitter:on adding "response" listener... +0ms
xhr:on adding "response" event listener: +0ms
async-event-emitter:on adding "response" listener... +0ms
setup-server:apply applying the interceptor... +0ms
async-event-emitter:activate set state to: ACTIVE +0ms
setup-server:apply activated the emiter! ACTIVE +0ms
setup-server retrieved global instance: undefined +2s
setup-server:apply no running instance found, setting up a new instance... +0ms
setup-server:setup applying all 2 interceptors... +0ms
setup-server:setup applying "ClientRequestInterceptor" interceptor... +0ms
http:apply applying the interceptor... +0ms
async-event-emitter:activate set state to: ACTIVE +0ms
http:apply activated the emiter! ACTIVE +0ms
http retrieved global instance: undefined +2s // => should this be NOT undefined here ?
http:apply no running instance found, setting up a new instance... +0ms
http:setup native "http" module patched! +0ms
http:setup native "https" module patched! +0ms
http set global instance! http +1ms
setup-server:setup adding interceptor dispose subscription +1ms
setup-server:setup applying "XMLHttpRequestInterceptor" interceptor... +0ms
xhr:apply applying the interceptor... +0ms
async-event-emitter:activate set state to: ACTIVE +0ms
xhr:apply activated the emiter! ACTIVE +0ms
xhr retrieved global instance: undefined +2s
xhr:apply no running instance found, setting up a new instance... +0ms
xhr:setup patching "XMLHttpRequest" module... +0ms
xhr:setup native "XMLHttpRequest" module patched! XMLHttpRequestOverride +0ms
xhr set global instance! xhr +0ms
setup-server:setup adding interceptor dispose subscription +0ms
setup-server set global instance! setup-server +1ms
[15:19:16.133][emittery:emit][undefined] Event Name: test-case-result
// notice there is no catch of any request, and hence the test fails
I have tried multiple strategies with different packages and the result is always the same. Here are some of the examples I have tried:
// same result
import fetch from 'cross-fetch';
global.fetch = globalThis.fetch = window.fetch = fetch;
console.log('fetch', window.fetch);
// segmentation fault (probably this libraries fault)
import fetch from 'isomorphic-unfetch';
global.fetch = globalThis.fetch = window.fetch = fetch;
console.log('fetch', window.fetch);
// same result
import fetch from 'isomorphic-fetch';
global.fetch = globalThis.fetch = window.fetch = fetch;
console.log('fetch', window.fetch);
But this is very weird because I have other tests that are mocking REST api requests and they work! (irregardless if polyfill is enabled or not)
I noticed you did a new release with some support for node 17 so I have updated just in case but the result is still the same i'm afraid.
Issue
I want to use
msw
in Jest unit tests only. However, GraphQL queries are not being intercepted. Our stack is React + Jest + Apollo GraphQL.I have already looked at https://github.com/mswjs/examples/tree/master/examples/graphql-react-apollo which is very helpful. Thanks for that.
Environment
msw: 0.19.3
nodejs: v12.14.0
npm: 6.13.4
Browser: Chrome 83.0.4103.97
I have 2 questions.
1. Do I need any extra setup to make
msw
work in node (Jest unit tests)?I believe this command is required to make
msw
work in browser for development. Do I need to do something similar for node?2. Is there any config needed for different url end point?
Our end point is
/prism
instead of/graphql
. Do I need to change any config to make it work besides setting up apollo client?Note: I have also tried changing our api url to
/graphql
but it still doesn't work.