msw: Server does not complete setup before next test suite executes in Mocha

Environment

Name Version
msw 0.27.0
node 12.19.0
OS Linux x86_64

Request handlers

Note: I have created a repo displaying the behavior here.

App.test.js

import React from 'react';
import { render } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import App from '../App';

describe('App tests', () => {
  const server = setupServer(
    rest.post('http://localhost:8080/api/getList', (req, res, ctx) => {
      return res(
        ctx.set('access-control-allow-origin', '*'),
        ctx.json({
          msgCode: 0,
          msgDesc: 'Request processed successfully',
          data: {
            data: {
              createdBy: 'CJ',
              createdDate: '02/18/2021',
            },
            pagination: {
              pageNo: req.body.pagination.pageNo,
              pageSize: req.body.pagination.pageSize,
              total: 1,
              sortedColumn: req.body.pagination.sortedColumn,
              sortedType: req.body.pagination.sortedType,
            },
          },
        })
      );
    })
  );

  before(() => {
    server.listen();
  });

  after(() => {
    server.close();
  });

  it('renders App without crashing', () => {
    render(<App />);
  });
});

Child.test.js

import React from 'react';
import { render } from '@testing-library/react';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
import Child from '../Child';

describe('Child tests', () => {
  const server = setupServer(
    rest.post('http://localhost:8080/api/getChildList', (req, res, ctx) => {
      return res(
        ctx.set('access-control-allow-origin', '*'),
        ctx.json({
          msgCode: 0,
          msgDesc: 'Request processed successfully',
          data: {
            data: {
              createdBy: 'CJ Child',
              createdDate: '02/18/2021',
            },
            pagination: {
              pageNo: req.body.pagination.pageNo,
              pageSize: req.body.pagination.pageSize,
              total: 1,
              sortedColumn: req.body.pagination.sortedColumn,
              sortedType: req.body.pagination.sortedType,
            },
          },
        })
      );
    })
  );

  before(() => {
    server.listen();
  });

  after(() => {
    server.close();
  });

  it('renders Child without crashing', () => {
    render(<Child />);
  });
});

Actual request

The actual request is simply performed on mount via the Fetch API (in our tests we are utilizing the isomorphic-fetch library to polyfill Fetch for Node).

function Child() {
  useEffect(() => {
    fetch('http://localhost:8080/api/getChildList', {
      method: 'POST',
      mode: 'cors',
      cache: 'no-cache',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        pagination: {
          pageSize: 10,
          pageNo: 0,
          sortedColumn: 'createdBy',
          sortedType: 'asc',
        },
      }),
    })
      .then(async (response) => {
        console.log('Fetch complete: ', await response.json());
      })
      .catch((error) => {
        console.error('Failed to fetch: ', error);
      });
  }, []);

  return (
    <React.Fragment>
      <h1>Child</h1>
    </React.Fragment>
  );
}

Current behavior

If we are running two test suites consecutively, in this case, App.test.js and then Child.test.js in quick succession, the second server is not fully set up before the test executes, resulting in something like this:

> mocha-ref@1.0.0 test:watch
> cross-env NODE_ENV=test mocha --timeout 10000 -w src/**/*.test.js



  App tests
    ✓ renders App without crashing
Fetch complete:  {
  msgCode: 0,
  msgDesc: 'Request processed successfully',
  data: {
    data: { createdBy: 'CJ', createdDate: '02/18/2021' },
    pagination: {
      pageNo: 0,
      pageSize: 10,
      total: 1,
      sortedColumn: 'createdBy',
      sortedType: 'asc'
    }
  }
}

  Child tests
    ✓ renders Child without crashing
Failed to fetch:  FetchError: request to http://localhost:8080/api/getChildList failed, reason: connect ECONNREFUSED 127.0.0.1:8080
    at ClientRequest.<anonymous> (/home/cjones26/JS/mocha-ref/node_modules/node-fetch/lib/index.js:1461:11)
    at ClientRequest.emit (events.js:314:20)
    at Socket.socketErrorListener (_http_client.js:428:9)
    at Socket.emit (events.js:314:20)
    at emitErrorNT (internal/streams/destroy.js:92:8)
    at emitErrorAndCloseNT (internal/streams/destroy.js:60:3)
    at processTicksAndRejections (internal/process/task_queues.js:84:21) {
  type: 'system',
  errno: 'ECONNREFUSED',
  code: 'ECONNREFUSED'
}


  2 passing (59ms)

ℹ [mocha] waiting for changes...

Expected behavior

> mocha-ref@1.0.0 test:watch
> cross-env NODE_ENV=test mocha --timeout 10000 -w src/**/*.test.js



  App tests
    ✓ renders App without crashing
Fetch complete:  {
  msgCode: 0,
  msgDesc: 'Request processed successfully',
  data: {
    data: { createdBy: 'CJ', createdDate: '02/18/2021' },
    pagination: {
      pageNo: 0,
      pageSize: 10,
      total: 1,
      sortedColumn: 'createdBy',
      sortedType: 'asc'
    }
  }
}

  Child tests
    ✓ renders Child without crashing
Fetch complete:  {
  msgCode: 0,
  msgDesc: 'Request processed successfully',
  data: {
    data: { createdBy: 'CJ Child', createdDate: '02/18/2021' },
    pagination: {
      pageNo: 0,
      pageSize: 10,
      total: 1,
      sortedColumn: 'createdBy',
      sortedType: 'asc'
    }
  }
}

  2 passing (59ms)

ℹ [mocha] waiting for changes...

Screenshots

Failed test: Failed test

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 16 (9 by maintainers)

Most upvoted comments

@cjones26 The magic here is the usage of the --file flag in the CLI (or in mocharc in this case). The server creation/cleanups are always rerun and skip the awkward handling of the lifecycle hooks behavior from mocha.

This behavior is slightly different than what I did originally where I created the server once, then set it to global.

Either works, but this is the best we can do for now. We’re going to continue to research supporting parallel mode - if we resolve that, I’ll let you know so you can update if necessary. Thanks again for providing this issue!

@kettanaito You might be right, but I’m not 100% sure and it’s kind of a pain to debug. By default, mocha runs sequentially and you have to opt-in to parallel=true. This seems to be more of a problem with how mocha is running tests, but I’m no mocha expert.

For example, in the repo as-is, if you remove the server.close() from each test file, it’ll run them all just fine but would lead to cleanup issues.

As an immediate solution that works with both sequential and parallel runs, what I’d recommend @cjones26 does is change the mocha configuration with these steps:

  1. Use root hooks - https://mochajs.org/#defining-a-root-hook-plugin (see below)
  2. Update mocha config (see below)
  3. Add hooks for the msw server (see below)
  4. Cleanup tests (see below)

In the given repro, you’d add this file:

// test/hooks.js
import { setupServer } from "msw/node";
import { rest } from "msw";

// export the server instance so you can `server.use` as needed
export const server = setupServer(
  rest.post("http://localhost:8080/api/getList", (req, res, ctx) => {
    return res(
      ctx.set("access-control-allow-origin", "*"),
      ctx.json({
        msgCode: 0,
        msgDesc: "Request processed successfully",
        data: {
          data: {
            createdBy: "CJ",
            createdDate: "02/18/2021",
          },
          pagination: {
            pageNo: req.body.pagination.pageNo,
            pageSize: req.body.pagination.pageSize,
            total: 1,
            sortedColumn: req.body.pagination.sortedColumn,
            sortedType: req.body.pagination.sortedType,
          },
        },
      })
    );
  }),
  rest.post("http://localhost:8080/api/getChildList", (req, res, ctx) => {
    return res(
      ctx.set("access-control-allow-origin", "*"),
      ctx.json({
        msgCode: 0,
        msgDesc: "Request processed successfully",
        data: {
          data: {
            createdBy: "CJ Child",
            createdDate: "02/18/2021",
          },
          pagination: {
            pageNo: req.body.pagination.pageNo,
            pageSize: req.body.pagination.pageSize,
            total: 1,
            sortedColumn: req.body.pagination.sortedColumn,
            sortedType: req.body.pagination.sortedType,
          },
        },
      })
    );
  })
);

exports.mochaHooks = {
  beforeAll: function () {
    server.listen();
  },
  afterEach: function () {
    server.resetHandlers();
  },
  afterAll: function () {
    server.close();
  },
};

Update mocha config to include the new hooks file:

// .mocharc.yaml
require:
  - "test/setupMocha.js"
  - "test/specHelper.js"
  - "test/hooks.js"

Refactor tests to look like:

import React from "react";
import { render } from "@testing-library/react";
import Child from "../Child";

describe("Child tests", () => {
  it("renders Child without crashing", () => {
    render(<Child />);
  });
});

and

import React from "react";
import { render } from "@testing-library/react";
import App from "../App";

describe("App tests", () => {
  it("renders App without crashing", async () => {
    render(<App />);
  });
});

As general guidance, I’d move the request handlers and server instance creation out into their own files so you can more easily reuse them in other places such as Storybook.

@marcosvega91 Thanks again! I merged that into my repo and into the PR for @cjones26. I think this is as good as we’re going to get for right now. We will have to investigate supporting parallel mode, which may or may not have to do with #474 as @kettanaito said 😃

@msutkowski – sure thing – as of now I am sure we are fine and won’t need parallel mode. Thanks again, closing this out.

@msutkowski I have looked into the code and update it a little bit using the before and after function in a global scope. I have add my code here. Let me know what you think

@msutkowski – you beat me to responding but yes it does appear to be something with the way that Mocha is working internally, and agree that it would be a pain to debug. I have not looked into the implementation of the functions, but my thought was that it could be helpful if server.listen and server.close both returned a promise which resolved once the server was fully started and/or fully cleaned up.

As for your suggestion above, I believe this will work for our use case and I will refactor our real project (not the provided sample) in order to validate this.

Thanks!