Separate Node.js unit, integration, and E2E tests using Jest projects
Full stack dev who loves to experiment, learn new things, and write stupid simple code.
Search for a command to run...
Full stack dev who loves to experiment, learn new things, and write stupid simple code.
No comments yet. Be the first to comment.
When you run your application inside a Docker container, it will be assigned process identifier (PID) 1. This particular PID is special in the Unix world. PID 1 is assigned to the very first process that the kernel starts, therefore it takes a specia...
Monorepos are all the rage right now. Modern projects are all using NX to set up a monorepo. But why would you introduce such a complex tool into your tech stack when something simple is often enough? Both Yarn and NPM include workspace management in...
I had a great weekend following the Learn Go with Tests online book. I spent about 15 hours going through the book and firing up a few practice projects. I know it’s not a lot to conclude, but I had enough experience with other languages to find the ...

Install dependencies # Yarn yarn add -D prettier eslint-config-prettier eslint-plugin-prettier # NPM npm install -D prettier eslint-config-prettier eslint-plugin-prettier By default your eslintConfig looks like this: "eslintConfig": { "extends"...
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.
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.
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.
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.
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 onlyMost 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
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 the unit projectmy-test.integration.js - should be in the integration projectmy-test.e2e.js - should be in the e2e projectCreate 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!
The project's source code is available on GitHub.
Check out the official documentation to learn more about Jest projects: