You can run unit tests in your npm project using Jest, and configuring it is mostly about telling npm how to invoke Jest.
Here’s a simple package.json with a test script:
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
When you run npm test, npm looks for the test script in package.json and executes it. In this case, it runs the jest command. If Jest is installed locally (as it should be in devDependencies), npm will find it in ./node_modules/.bin/jest and run that specific executable.
Let’s say you have a file named math.js:
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
And a corresponding test file, math.test.js:
// math.test.js
const { add, subtract } = require('./math');
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
test('subtracts 5 - 2 to equal 3', () => {
expect(subtract(5, 2)).toBe(3);
});
Running npm test will execute Jest, which will automatically discover and run math.test.js because of the .test.js naming convention. The output will look something like this:
> my-project@1.0.0 test
> jest
PASS ./math.test.js
✓ adds 1 + 2 to equal 3 (2ms)
✓ subtracts 5 - 2 to equal 3
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.548s, estimated 1s
The power comes when you want to pass specific arguments to Jest. For example, to only run tests in a specific file, you can modify the script:
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"test": "jest --watch"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
Now, npm test will run Jest in watch mode, automatically re-running tests when files change.
You can also pass arguments directly when running npm test:
npm test -- path/to/your/test.js
The -- tells npm to pass all subsequent arguments directly to the script being executed. So, npm test -- path/to/your/test.js is equivalent to running jest path/to/your/test.js directly.
To further customize Jest’s behavior without always typing flags, you can add a jest configuration block to your package.json:
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.7.0"
},
"jest": {
"testMatch": [
"**/__tests__/**/*.js?(x)",
"**/?(*.)+(spec|test).js?(x)"
],
"moduleFileExtensions": [
"js",
"json",
"jsx",
"ts",
"tsx",
"node"
],
"collectCoverage": true,
"coverageDirectory": "coverage"
}
}
Here, testMatch defines which files Jest should look for tests in. The default is usually __tests__/** and **/?(*.)+(spec|test).*. moduleFileExtensions tells Jest which file extensions it should consider when require()ing modules. collectCoverage enables coverage reporting, and coverageDirectory specifies where the reports should be saved.
When you run npm test with this configuration, Jest will use these settings automatically. The output will now include coverage information if tests pass:
> my-project@1.0.0 test
> jest
PASS ./math.test.js
✓ adds 1 + 2 to equal 3 (2ms)
✓ subtracts 5 - 2 to equal 3
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Lines
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
math.js | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 0.721s, estimated 1s
The jest configuration block in package.json is just one way to configure Jest. You can also use a jest.config.js file or a jest.config.ts file at the root of your project. This is often preferred for more complex configurations as it keeps your package.json cleaner.
For example, jest.config.js would look like this:
// jest.config.js
module.exports = {
testMatch: [
"**/__tests__/**/*.js?(x)",
"**/?(*.)+(spec|test).js?(x)"
],
moduleFileExtensions: [
"js",
"json",
"jsx",
"ts",
"tsx",
"node"
],
collectCoverage: true,
coverageDirectory: "coverage"
};
If both package.json and a jest.config.js file exist, the jest.config.js file takes precedence.
One common pattern is to have multiple test scripts. For instance, you might have a script for running tests normally and another for running them with coverage:
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"devDependencies": {
"jest": "^29.7.0"
}
}
Now you can run npm run test:watch or npm run test:coverage to execute those specific configurations. The npm test command is a special alias for npm run test.
The most surprising thing about Jest configuration is how seamlessly it integrates with the Node.js module system, allowing you to require or import your application code directly into your test files, and Jest handles the bundling and execution without needing a separate build step for your tests.
When you’re working with modern JavaScript features like ES modules (import/export) or TypeScript, you’ll often need to configure Jest to transpile your code. This is typically done by adding Babel or ts-jest as a Jest transformer.
For Babel, you’d install:
npm install --save-dev @babel/core @babel/preset-env babel-jest
And configure package.json or jest.config.js:
// package.json
{
// ...
"jest": {
// ... other config
"transform": {
"^.+\\.(js|jsx|ts|tsx)$": "babel-jest"
}
}
}
Or for TypeScript with ts-jest:
npm install --save-dev typescript ts-jest
// package.json
{
// ...
"jest": {
// ... other config
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
"testEnvironment": "node" // or "jsdom" if testing browser environments
}
}
This configuration tells Jest to use babel-jest (or ts-jest) to transform files matching the regex ^.+\\.(js|jsx|ts|tsx)$ before running the tests. Without this, Jest would likely throw errors when encountering modern syntax.
The next step is often exploring how to mock dependencies and manage asynchronous operations within your tests.