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

Response not returned if using jest.useFakeTimers() #448

Closed
bud-mo opened this issue Nov 6, 2020 · 7 comments
Closed

Response not returned if using jest.useFakeTimers() #448

bud-mo opened this issue Nov 6, 2020 · 7 comments
Labels
bug Something isn't working scope:node Related to MSW running in Node

Comments

@bud-mo
Copy link

bud-mo commented Nov 6, 2020

Environment

Name Version
msw 0.21.3
node 12
OS Linux / OSX

Request handlers

import { setupServer } from 'msw/node'
import { rest } from 'msw'

const server = setupServer(
    rest.post(
        '/api/test',
        async (req, res, ctx) => res(
            ctx.json({ test: 'hello' })
        ),
    ),
);

server.listen()

Actual request

const res = await fetch("/api/test", {
      method: "POST",
      body: JSON.stringify({}),
});
const obj = await res.json();
expect(obj).toMatchInlineSnapshot(`
    Object {
        "test": "hello",
    }
`);

Current behavior

Using jest.useFakeTimers() the response is not returned by the server provided by msw.

The handler is called correctly, but the response hangs.

Expected behavior

Use jest.useFakeTimers() into some tests without affecting msw.

Screenshots

> jest

 FAIL  src/example.test.js (5.762 s)
  SOAP Utils
    ✕ Test (5012 ms)

  ● SOAP Utils › Test

    : Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

      1 | describe("SOAP Utils", () => {
    > 2 |   test(`Test`, async () => {
        |   ^
      3 |     const res = await fetch("/api/test", {
      4 |       method: "POST",
      5 |       body: JSON.stringify({}),

      at new Spec (node_modules/jest-jasmine2/build/jasmine/Spec.js:116:22)
      at Suite.<anonymous> (src/example.test.js:2:3)
      at Object.<anonymous> (src/example.test.js:1:1)

-------------|---------|----------|---------|---------|-------------------
File         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-------------|---------|----------|---------|---------|-------------------
All files    |     100 |      100 |     100 |     100 |                   
 handlers.js |     100 |      100 |     100 |     100 |                   
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   1 passed, 1 total
Time:        6.267 s
Ran all test suites.

Here You can find a repository to reproduce the issue: https://github.com/bud-mo/msw-example

@bud-mo bud-mo added bug Something isn't working scope:node Related to MSW running in Node labels Nov 6, 2020
@kettanaito
Copy link
Member

Hey, @bud-mo. Thanks for reporting this, and especially for the reproduction repository.

I'm debugging the issue and I can see that the response Promise resolves correctly, as we replace native setTimeout with require('timers').setTimeout. However, the test still timeouts. I believe that fake timers are not used properly in the test. Perhaps they need to be advanced for the test to conclude?

I'm researching this at the moment, but I don't have much experience with fake timers, so it's going to take some time. If you are familiar with them, could you double check how you've used them on other projects, or on the web?

@kettanaito
Copy link
Member

kettanaito commented Nov 7, 2020

However, the whatwg-fetch library you use to polyfill fetch for tests does use native setTimeout to handle XHR events:

xhr.onload = function() {
  var options = {
    status: xhr.status,
    statusText: xhr.statusText,
    headers: parseHeaders(xhr.getAllResponseHeaders() || '')
  };
  options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL');
  var body = 'response' in xhr ? xhr.response : xhr.responseText;
  setTimeout(function() {
    resolve(new Response(body, options));
  }, 0);
};

Since stubbing instances affects the entire NodeJS process, using fake timers with Jest effectively stubs all setTimeout functions, including those function calls in third-party libraries like whatwg-fetch.

That may be the reason your test timeouts. That may also be the reason why if you console.log(obj) you will see the mocked response after the test has timed out. To my best knowledge, it looks like the fake timers must be progressed in order for those native setTimeout calls to execute.

@kettanaito
Copy link
Member

I've also found a few threads that mention that you cannot use async/await syntax with fake timers in Jest:

You can find more info of why that's so in the first referenced link.

@kettanaito
Copy link
Member

If you swap whatwg-fetch with another request issuing library you can see the test passing, although still using fake timers and MSW. Here's your test using native http module to make a request (no setTimeout calls):

import http from 'http'

test(`Test`, (done) => {
  let resBody = ''

  // Resolving against the current location, because relative URLs don't exist in NodeJS.
  const req = http.request(new URL('/api/test', location.href), {
    method: 'POST',
  })

  req.on('response', (res) => {
    res.on('data', (chunk) => (resBody += chunk))
    res.on('end', () => {
      const resBodyObject = JSON.parse(resBody)
      expect(resBodyObject).toMatchInlineSnapshot(`
      Object {
        "test": "hello",
      }
    `)

      done()
    })
  })

  req.write(JSON.stringify({}))
  req.end()
})

Suggestions

I don't think this is the issue with MSW. I suggest you to:

  • Raise the issue in the whatwg-fetch or jest repositories, the problem lies in fake timers + fetch polyfill + (potentially) async/await syntax.
  • Replace whatwg-fetch polyfill with a compatible implementation that doesn't rely on setTimeout.
  • Figure out how to progress whatwg-fetch timers when using fake timers in Jest.

I'm always willing to reopen the issue if we find sufficient proof it's MSW problem.

@bud-mo
Copy link
Author

bud-mo commented Nov 9, 2020

@kettanaito Thank you for the support!

@kettanaito
Copy link
Member

Please let us know what would be the solution to this. We could include fake timers in some usage examples for others to follow. Thanks!

@bud-mo
Copy link
Author

bud-mo commented Nov 9, 2020

@kettanaito I ended up using node-fetch.

In my case is easy to use because I am wrapping the fetch to inject some auth headers to each request, so I have mocked that function to use the fetch provided by node-fetch, I only need to add the new URL('/api/test', location.href) line and it works great.

For documentation purpose I think we can provide an example with a simple fetch wrapper around node-fetch.

I created a branch with an updated working example with a possible solution, this patch can be a starting point:
bud-mo/msw-example@b4ba10f

ChengYanJin pushed a commit to scality/metalk8s that referenced this issue Apr 20, 2021
@github-actions github-actions bot locked and limited conversation to collaborators Nov 8, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
bug Something isn't working scope:node Related to MSW running in Node
Projects
None yet
Development

No branches or pull requests

2 participants