
Setting up Jest for your testing environment is quite straightforward, and it’s a game changer for JavaScript developers. First, you’ll want to install Jest using npm. If you haven’t already initialized your project with npm, do that first.
npm init -y
Next, you can install Jest as a development dependency. This ensures that it won’t affect your production environment.
npm install --save-dev jest
Once installed, you need to update your package.json to include a test script. This allows you to run your tests easily from the command line.
{
"scripts": {
"test": "jest"
}
}
Now you can create a basic test file. By default, Jest looks for files inside a __tests__ directory or files with a .test.js suffix. Let’s create a simple test file to get you started. You might want to create a directory called tests and add a file named example.test.js.
const sum = (a, b) => a + b;
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
After saving your test file, you can run your tests by executing the following command:
npm test
Jest will automatically find and run the tests you’ve written. If everything is set up correctly, you should see an output indicating that your tests passed successfully. If you want to run Jest in watch mode, which is useful during development, you can run:
npm test -- --watch
This will watch for changes in your files and rerun the tests automatically whenever you save. It’s a fantastic way to streamline your development process. Now that you’ve set up Jest, you can dive into writing your first unit tests. But before we get to that, it’s good to familiarize yourself with some of Jest’s features that make it incredibly useful for testing.
One of those features is the ability to mock functions and modules. This can be particularly useful when you want to isolate the unit of code you’re testing. For instance, if you’re testing a function that relies on an API call, you can mock that call to ensure your test runs quickly and doesn’t depend on external factors.
const fetchData = require('./fetchData');
jest.mock('./fetchData');
test('fetches data successfully', async () => {
fetchData.mockResolvedValueOnce('data');
const data = await fetchData();
expect(data).toBe('data');
});
Hastraith Stylus Pen for iPad(2018-2026)-13 Mins Fast Charge with Tilt Sensitivity & Palm Rejection for iPad 11/10/9/8/7/6th Gen, Air 5/4/3/M4/M3/M2, Pro 13"/12.9"/11"/M4, Mini 7/6/5th, White
$15.99 (as of June 3, 2026 23:09 GMT +00:00 - More infoProduct prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on [relevant Amazon Site(s), as applicable] at the time of purchase will apply to the purchase of this product.)Write a basic unit test
So, let’s get down to brass tacks. Writing a unit test is all about isolating a small, testable piece of your code-a “unit”-and verifying that it behaves exactly as you expect. The simplest units are pure functions: they take some input, do something with it, and return an output, without any sneaky side effects like talking to a database or writing to a file. We’ve already seen a trivial sum function. Let’s build on that with a slightly more realistic module. Create a file named calculator.js.
// calculator.js
const calculator = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
};
module.exports = calculator;
Now, let’s write the test file for this module. We’ll call it calculator.test.js. Jest will pick it up automatically. Notice how we import the module we want to test.
// calculator.test.js
const calculator = require('./calculator');
test('adds 2 + 3 to equal 5', () => {
expect(calculator.add(2, 3)).toBe(5);
});
test('subtracts 5 - 2 to equal 3', () => {
expect(calculator.subtract(5, 2)).toBe(3);
});
This works, but as your test suite grows, you’ll want to organize your tests. That’s where the describe block comes in. It groups related tests together, which makes your test output much easier to read. It’s just a good habit to get into.
// calculator.test.js
const calculator = require('./calculator');
describe('calculator', () => {
test('adds 2 + 3 to equal 5', () => {
expect(calculator.add(2, 3)).toBe(5);
});
test('subtracts 5 - 2 to equal 3', () => {
expect(calculator.subtract(5, 2)).toBe(3);
});
test('multiplies 4 * 3 to equal 12', () => {
expect(calculator.multiply(4, 3)).toBe(12);
});
});
So far, we’ve only used the toBe matcher, which uses Object.is to test for exact equality. This is great for primitive types like numbers, strings, and booleans. But what happens when you’re dealing with objects or arrays? toBe will check if they are the *same* object in memory, not if they have the same values. This trips up a lot of people. For checking object or array equality, you need to use toEqual.
test('object assignment', () => {
const data = {one: 1};
data['two'] = 2;
// This will fail because they are different objects in memory
// expect(data).toBe({one: 1, two: 2});
// This will pass because it checks for value equality
expect(data).toEqual({one: 1, two: 2});
});
Jest comes with a whole suite of these “matcher” functions that let you assert all kinds of things about your code. You can check for truthiness, falsiness, null, undefined, and more. For example, if you have a function that might return null, you can test for that explicitly.
test('null, undefined, and truthiness', () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
const z = 0;
expect(z).not.toBeNull();
expect(z).toBeDefined();
expect(z).not.toBeUndefined();
expect(z).not.toBeTruthy();
expect(z).toBeFalsy();
});
This is just scratching the surface. You can test if an array or string contains a specific item, if a number is greater than or less than another, or even if a value is close to another for floating-point numbers where precision can be an issue. The key is to pick the most specific matcher for your use case. It makes your tests clearer and your failure messages more helpful. For instance, testing an array for a specific element is much better done with toContain.
Run and debug your test
When it comes to debugging your tests, Jest provides a built-in way to help you figure out what’s going wrong. If a test fails, Jest will give you a detailed output that includes the expected value and the received value. This is a good starting point for debugging. However, sometimes you need a more hands-on approach.
One effective method to debug your tests is to use the debugger statement. You can insert debugger; in your test code, and when you run your tests in a debugging environment, it will pause execution at that line. This allows you to inspect variables and the state of your application at that exact moment.
test('debugging example', () => {
const result = calculator.add(2, 3);
debugger; // Execution will pause here
expect(result).toBe(5);
});
To run your tests in a debugging environment, you can use Node.js’s built-in debugger. You can start your Jest tests with the --inspect flag:
node --inspect-brk node_modules/.bin/jest --runInBand
This command tells Node.js to run Jest with the debugger enabled and to pause execution before the first line of your tests. You can then open Chrome, navigate to chrome://inspect, and connect to the Node instance. This gives you a powerful debugging interface where you can step through your code, inspect variables, and see the call stack.
Another useful feature is Jest’s snapshot testing. This allows you to capture the output of your component or function and compare it against a reference snapshot. If the output changes, Jest will alert you, which is particularly useful for testing React components or any output that is complex and subject to change.
test('matches the snapshot', () => {
const tree = renderer.create().toJSON();
expect(tree).toMatchSnapshot();
});
When you run this test for the first time, Jest will create a snapshot file in a __snapshots__ directory. Future test runs will compare the output against this snapshot. If there are changes, Jest will fail the test and show you a diff, allowing you to review what changed.
However, be cautious with snapshot testing. If you’re not careful, you might end up with large snapshots that make it hard to track changes. Always ensure that your snapshots are meaningful and represent a specific state of your application. If the output is supposed to change frequently, consider whether a snapshot test is the right choice.
As your test suite grows, you’ll want to keep things organized. Using a consistent naming convention for your test files and directories can help. Group related tests into directories that reflect their functionality. This structure not only makes it easier to find tests but also helps in understanding the overall architecture of your codebase.
Another aspect of improving your testing skills is to regularly refactor your tests as you would with your production code. If you find yourself repeating the same logic across multiple tests, consider abstracting that logic into helper functions. This not only reduces duplication but also makes your tests cleaner and more maintainable.
const setup = () => {
// Common setup logic
};
beforeEach(() => {
setup();
});
test('first test', () => {
// Test logic
});
test('second test', () => {
// Test logic
});
By using lifecycle methods like beforeEach or afterEach, you can run setup or teardown logic automatically, ensuring that each test starts with a clean slate. This can be particularly useful when testing components that maintain state or when you need to reset mocks.
Lastly, don’t forget about the importance of code coverage. Jest offers a built-in coverage tool that can help you determine which parts of your code are being tested and which are not. You can enable coverage reporting by running:
npm test -- --coverage
This will generate a coverage report that shows you the percentage of your code that is covered by tests, helping you identify untested areas. Aim for high coverage, but remember that 100% coverage doesn’t guarantee your code is bug-free. Focus on writing meaningful tests that cover edge cases and ensure your code behaves as expected.
Improve your testing skills
While code coverage is a useful metric, it’s a vanity metric if the tests themselves aren’t robust. A test that covers a line of code but doesn’t properly assert its behavior is worse than no test at all because it gives you a false sense of security. The real skill is in writing tests that can precisely target a piece of logic and verify its correctness in isolation. This is where mocking comes in. You can’t properly unit test a function that calls an external API without being able to control what that API returns.
Mocking allows you to replace dependencies with “fake” versions that you control. Let’s say you have a service that fetches user data and then transforms it. You don’t want your test to make a real network request. It would be slow, unreliable, and dependent on an external system. Instead, you mock the API module.
// api.js
const fetchUserData = (userId) => {
// In a real app, this would make a network request
// For this example, we'll just return a promise
return Promise.resolve({ id: userId, name: 'Leanne Graham' });
};
module.exports = { fetchUserData };
// userService.js
const api = require('./api');
const getUserName = async (userId) => {
const user = await api.fetchUserData(userId);
return user.name.toUpperCase();
};
module.exports = { getUserName };
Now, to test userService.js, you tell Jest to create a mock of the api.js module. This replaces all its exported functions with dummy mock functions. Then, inside your test, you can provide a specific fake implementation for the function you care about.
// userService.test.js
const { getUserName } = require('./userService');
const api = require('./api');
// Tell Jest to mock the entire api module
jest.mock('./api');
test('getUserName fetches and returns the uppercase user name', async () => {
// Provide a mock implementation for fetchUserData for this specific test
api.fetchUserData.mockResolvedValue({ id: 1, name: 'Joel Spolsky' });
const userName = await getUserName(1);
expect(userName).toBe('JOEL SPOLSKY');
// You can also assert that the mock was called correctly
expect(api.fetchUserData).toHaveBeenCalledWith(1);
});
Sometimes you don’t want to replace a function entirely; you just want to know if it was called. This is what spies are for. Using jest.spyOn(), you can wrap an existing function to track its calls, arguments, and return values, while still allowing the original implementation to execute. It’s perfect for verifying side effects, like logging.
// logger.js
const logger = {
info: (message) => {
console.log(INFO: ${message});
},
};
// someModule.js
const processData = (data) => {
logger.info(Processing ${data.length} items.);
// ... actual processing logic
};
// someModule.test.js
const spy = jest.spyOn(logger, 'info').mockImplementation(() => {}); // Mock implementation to prevent console output
processData([1, 2, 3]);
expect(spy).toHaveBeenCalledWith('Processing 3 items.');
// It's good practice to restore the original implementation after the test
spy.mockRestore();
Testing asynchronous code is another area where developers often get stuck. If you don’t handle it correctly, your tests might finish before your async operation does, leading to false positives. The modern way to handle this in JavaScript is with async/await. You simply declare your test function as async and then await any promises inside it. Jest will wait for the promise to resolve before finishing the test.
// fetchData.js
const fetchData = () => new Promise(resolve => setTimeout(() => resolve('peanut butter'), 100));
// fetchData.test.js
test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
Jest also provides special matchers, .resolves and .rejects, that make this even cleaner. You can chain them off an expect call that wraps a promise. This is a very declarative way to test asynchronous outcomes.
// fetchData.test.js
test('the data is peanut butter with .resolves', () => {
return expect(fetchData()).resolves.toBe('peanut butter');
});
const fetchError = () => new Promise((_, reject) => setTimeout(() => reject(new Error('error')), 100));
test('the fetch fails with an error with .rejects', () => {
return expect(fetchError()).rejects.toThrow('error');
});
For older code that uses callbacks instead of promises, Jest supports that too. You can write your test to accept a single argument, which is conventionally named done. Jest will wait until the done callback is called before finishing the test. If done() is never called, the test will fail due to a timeout, which is exactly what you want to happen if your callback is never invoked.
// callbackExample.js
function fetchDataWithCallback(callback) {
setTimeout(() => {
callback(null, 'some data');
}, 100);
}
// callbackExample.test.js
test('the data is "some data" using a callback', done => {
function callback(error, data) {
if (error) {
done(error);
return;
}
try {
expect(data).toBe('some data');
done();
} catch (error) {
done(error);
}
}
fetchDataWithCallback(callback);
});
