Start Server in Jest


Here's how to start a server in Jest.

Test Scenario

Jest is a test environment. It'll run all sorts of tests. Most commonly, you might set up a unit test. You'll import some src from your test, exercise it and make assertions on output.

But sometimes you might want to test something larger than a unit. Maybe multiple units integrating together. Or maybe interact with your code at the network level.

That's what we're going to do here: We're going to start our server that serves an HTTP API, make requests to it and then assert the response. We'll have the jest process start the API that will be under test.

Test Script

It'll be nice to have an npm-script for this, so let's add it to package.json:

{
  "test:integ": "jest --config jest.config.integ.js",
}

Configure Jest

Let's implement that jest.config.integ.js file:

module.exports = {
  globalSetup: "./test/integ/setup.js",
  globalTeardown: "./test/integ/teardown.js",
  testPathIgnorePatterns: ["/node_modules/", "test/unit"],
  testTimeout: 5000,
};
  • globalSetup will be code that runs before any test suite runs.
  • globalTeardown will be code that at the very end, after all test suites run.
  • testPathIgnorePatterns is going to make sure that this special group of tests, integ or "integration" tests, ignore the unit tests and don't run them when using this script.
  • testTimeout will allow a lot of time for the tests to run. These will be longer-running than a common unit test.

Implement globalSetup

We'll write that setup.js file now:

const { spawn } = require("child_process");

module.exports = function setup() {
  return new Promise((resolve) => {
    const child = spawn("node src/server.js", ["--port", "3005"], {
      cwd: __dirname + "/../..",
    });
    globalThis.__INTEG_TEST_SERVER_PID__ = child.pid;
    child.stderr.on("data", (data) => {
      const str = data.toString();
      console.log("[server]", str);
      if (str.includes("localhost:3005")) {
        setTimeout(resolve, 200);
      }
    });
  });
};
  • spawn is being used to fork a new process from node. In this case, we're starting our server with node src/server.js.
  • We're setting the directory that we're running this command using the cwd option. This will bring us out of our test/integ directory, where setup.js is located.
  • spawn creates a child process that has a process id, or pid. We savve this for later, so we can stop the process.
  • on("data") is going to create a listener for output from the server. We want the server to be fully started before we resolve the async setup function that we're in. The way we do this for this server is read the server location log ("localhost:3005") from stderr. Your own server will likely have something different here.

Implement globalTeardown

Now how do we stop this? Let's implement teardown.js:

module.exports = function teardown() {
  process.kill(globalThis.__INTEG_TEST_SERVER_PID__);
};

Remember that pid that we saved? We can access setup.js globals here in teardown.js.

Start to Finish

So how does this work? We write a test. It makes HTTP requests against localhost:3005. We set up the above code to be able to start our API server. We npm run test:integ. It'll run setup.js, starting the server. Then our test suite will run. It'll be able to contact localhost:3005 and make the assertions in our test. We'll run all our tests in the suite. Jest will trigger teardown.js to run. It'll kill the server process. We'll retire happy programmers.