Separate Node.js unit, integration, and E2E tests using Jest projects
The classic test pyramid is made up of the unit, integration, and end-to-end tests. Unit tests are supposed to run fast after each change in your IDE and give immediate feedback. In contrast, integration and E2E tests are slow and are run on-demand. Sometimes you want to run all your tests, sometimes you want to run your E2E tests and nothing else. Fortunately, Jest provides a feature called projects that solves this problem relatively easily.
Whatever test pyramid you follow, you will always have faster-slower tests. So let's figure out how to separate them using Jest projects.
Initialize the project
Create an empty directory and initialize your Node.js project:
mkdir jest-projects
cd jest-projects
npm init -y
Of course, you will need Jest to run your tests, so install it as a development dependency:
npm install -D jest
By default, Jest treats any JS file in the __test__
directory as a test file, and Jest will try to run it. First, create the directory and create a test.js
file.
mkdir __tests__
touch __tests__/test.js
Add a simple test
__tests__/test.js
it("test works", () => {
expect(1 + 1).toBe(2);
});
Now ask Jest to run the test:
npx jest
You should see a successful test run.
PASS __tests__/test.js
√ test works (2 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.264 s
Ran all test suites.
Integration and E2E tests
Right now, Jest runs all the tests in the __tests__
folder. So let's create a separate directory for unit, integration, and E2E tests.
mkdir __tests__/unit
mkdir __tests__/integration
mkdir __tests__/e2e
Move the existing test.js
file into the unit
folder and copy-paste the file to the integration
and e2e
directory.
At this point, you should have the following directory tree:
├── __tests__
│ ├── e2e
│ │ └── test.js
│ ├── integration
│ │ └── test.js
│ └── unit
│ └── test.js
├── node_modules
├── package-lock.json
└── package.json
If you run Jest, you will see that all tests are run by default.
$ npx jest
PASS __tests__/integration/test.js
PASS __tests__/e2e/test.js
PASS __tests__/unit/test.js
Test Suites: 3 passed, 3 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 1.043 s
Ran all test suites.
Configure Jest projects
You will need four different configuration files. One configuration file will be the container file that references all your Jest projects. Rest three files are for unit, integration, and e2e projects.
jest.config.js
module.exports = {
projects: [
"<rootDir>/jest.unit.config.js",
"<rootDir>/jest.integration.config.js",
"<rootDir>/jest.e2e.config.js",
],
};
jest.unit.config.js
module.exports = {
displayName: "unit",
testMatch: ["**/__tests__/**/unit/**/*.[jt]s?(x)"],
};
jest.integration.config.js
module.exports = {
displayName: "integration",
testMatch: ["**/__tests__/**/integration/**/*.[jt]s?(x)"],
};
jest.e2e.config.js
module.exports = {
displayName: "e2e",
testMatch: ["**/__tests__/**/e2e/**/*.[jt]s?(x)"],
};
As you can see, these configuration files are very similar, except for the main configuration file that references the projects. These configuration files specify a display name and a glob pattern to match the test files.
Now, run your tests
$ npx jest
PASS integration __tests__/integration/test.js
PASS e2e __tests__/e2e/test.js
PASS unit __tests__/unit/test.js
Test Suites: 3 passed, 3 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 0.755 s, estimated 1 s
Ran all test suites in 3 projects.
Jest successfully recognized the three separate projects. You can see the display name of the project next to the file path.
What if you want to run specific tests? Jest CLI provides many options, but in this case, you should be interested in --selectProjects
and --ignoreProjects
.
Let's run e2e tests only
$ npx jest --selectProjects e2e
Running one project: e2e
PASS e2e __tests__/e2e/test.js
√ test works (1 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.363 s, estimated 1 s
Ran all test suites.
Or run all tests, except e2e tests
$ npx jest --ignoreProjects e2e
Running 2 projects:
- integration
- unit
PASS integration __tests__/integration/test.js
PASS unit __tests__/unit/test.js
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.559 s, estimated 1 s
Ran all test suites in 2 projects.
NPM scripts
You will probably get tired of typing the Jest commands all the time. So write a few NPM scripts so your fingers can get some rest.
package.json
{
"name": "jest-projects",
"version": "1.0.0",
"scripts": {
"test": "jest",
"test:unit": "jest --selectProjects unit",
"test:integration": "jest --selectProjects integration",
"test:e2e": "jest --selectProjects e2e"
},
"devDependencies": {
"jest": "^28.1.3"
}
}
This will provide you with four scripts:
npm test
- Runs all tests (unit, integration and E2E)npm run test:unit
- Runs unit tests onlynpm run test:integration
- Runs integration tests onlynpm run test:e2e
- Runs E2E tests only
Global setup and teardown modules
Most of the time, integration and E2E tests require some kind of setup and teardown before the test suite. Jest provides globalSetup
and globalTeardown
options in the configuration file. Let's set it up for the E2E tests.
jest.e2e.config.js
module.exports = {
displayName: "e2e",
testMatch: ["**/__tests__/**/e2e/**/*.[jt]s?(x)"],
testPathIgnorePatterns: [
"<rootDir>/__tests__/e2e/_setup.js",
"<rootDir>/__tests__/e2e/_teardown.js",
],
globalSetup: "<rootDir>/__tests__/e2e/_setup.js",
globalTeardown: "<rootDir>/__tests__/e2e/_teardown.js",
};
This config will tell Jest to run the __tests__/e2e/_setup.js
before all E2E test suites. And run <rootDir>/__tests__/e2e/_teardown.js
after all E2E test suites.
Now, you might wonder why testPathIgnorePatterns
contains the _setup
and _teardown
files. Take a look at the testMatch
's value. It matches any .js
, .ts
, .jsx
and .tsx
files in any e2e
directory. Therefore this configuration simply matches the setup and teardown files as well. If you don't ignore them, Jest will complain that these files don't contain any tests.
Create the setup and teardown modules
__tests__/e2e/_setup.js
module.exports = () => {
console.log("E2E setup");
};
__tests__/e2e/_teardown.js
module.exports = () => {
console.log("E2E teardown");
};
Now, if you run your E2E tests, you will see the messages in your terminal
$ npm run test:e2e
> [email protected] test:e2e
> jest --selectProjects e2e
Running one project: e2e
Determining test suites to run...E2E setup
PASS e2e __tests__/e2e/test.js
√ test works (1 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.336 s, estimated 1 s
Ran all test suites.
E2E teardown
Tests by filename suffixes
You might prefer a different setup for your test files. Some projects use different directories, some projects colocate tests, and some projects just use naming conventions to differentiate between tests.
Previously, you separated your tests by using different directories. Now let's assume you want to do the following:
my-test.unit.js
- should be in theunit
projectmy-test.integration.js
- should be in theintegration
projectmy-test.e2e.js
- should be in thee2e
project
Create an empty lib
directory, this is where you will store your source code files:
mkdir lib
Write a simple add
function:
lib/add.js
module.exports.add = (a, b) => a + b;
Create a unit, integration, and E2E test file for this simple add
function.
lib/add.unit.js
const { add } = require("./add");
it("add(1, 1) should return 2", () => {
expect(add(1, 1)).toBe(2);
});
lib/add.int.js
const { setTimeout } = require("node:timers/promises");
const { add } = require("./add");
it("add(1, 1) should return 2 (1s delay)", async () => {
await setTimeout(1000);
expect(add(1, 1)).toBe(2);
});
lib/add.e2e.js
const { setTimeout } = require("node:timers/promises");
const { add } = require("./add");
it("add(1, 1) should return 2 (3s delay)", async () => {
await setTimeout(3000);
expect(add(1, 1)).toBe(2);
});
Integration and E2E tests include a delay to simulate the slowness of these tests.
At this point, if you try to run any of your test commands, you notice that none of these are running any of the new tests. Let's modify the configuration files to run colocated test files as well in the lib
directory.
jest.unit.config.js
module.exports = {
displayName: "unit",
testMatch: [
"**/__tests__/**/unit/**/*.[jt]s?(x)",
"<rootDir>/lib/**/*.unit.[jt]s?(x)",
],
};
jest.integration.config.js
module.exports = {
displayName: "integration",
testMatch: [
"**/__tests__/**/integration/**/*.[jt]s?(x)",
"<rootDir>/lib/**/*.int.[jt]s?(x)",
],
};
jest.e2e.config.js
module.exports = {
displayName: "e2e",
testMatch: [
"**/__tests__/**/e2e/**/*.[jt]s?(x)",
"<rootDir>/lib/**/*.e2e.[jt]s?(x)",
],
testPathIgnorePatterns: [
"<rootDir>/__tests__/e2e/_setup.js",
"<rootDir>/__tests__/e2e/_teardown.js",
],
globalSetup: "<rootDir>/__tests__/e2e/_setup.js",
globalTeardown: "<rootDir>/__tests__/e2e/_teardown.js",
};
Each configuration has a new element in the testMatch
array. Each matches its respective desired file naming format. Note that in this case <rootDir>/lib
is used. So the lib
directory must reside in the root directory of your project to be matched. Whereas **/__tests__
matches nested ___tests__
directories as well.
Now, let's test what you have worked on
$ npm test
> [email protected] test
> jest
Determining test suites to run...E2E setup
PASS unit __tests__/unit/test.js
PASS integration __tests__/integration/test.js
PASS e2e __tests__/e2e/test.js
PASS unit lib/add.unit.js
PASS integration lib/add.int.js
PASS e2e lib/add.e2e.js
Test Suites: 6 passed, 6 total
Tests: 6 passed, 6 total
Snapshots: 0 total
Time: 4.086 s
Ran all test suites in 3 projects.
E2E teardown
Jest now runs the tests in the lib
directory without any issues! The test took 4 seconds due to delays in the new integration and E2E tests.
Now try to run unit tests only
$ npm run test:unit
> [email protected] test:unit
> jest --selectProjects unit
Running one project: unit
PASS unit lib/add.unit.js
PASS unit __tests__/unit/test.js
Test Suites: 2 passed, 2 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.542 s, estimated 1 s
Ran all test suites.
It took half a second, and no integration and E2E tests were run. Looks good!
You are done! Great job!
The project's source code is available on GitHub.
Check out the official documentation to learn more about Jest projects: