Who this is for: Developers ready to dive into writing practical tests. Jest is the most widely adopted testing framework in the JavaScript ecosystem. In this module, we move beyond theory and get our hands dirty with real-world test cases, from configuration to React component testing.
Learning Objectives
By the end of this module, you will be able to:
Configure Jest for modern JavaScript/TypeScript projects
Write robust unit tests using Jest's extensive matchers
Handle asynchronous code confidently
Use mocks and spies to isolate logic
Test React components effectively with React Testing Library
Implement and interpret Snapshot tests
Generate and read coverage reports
Setup & Configuration
Jest is an "all-in-one" powerhouse. It includes a test runner, an assertion library, and a mocking framework natively.
To set up Jest in a TypeScript project:
bash
This creates a jest.config.js file. A standard robust configuration often looks like this:
javascript
Matchers: Beyond toBe
Jest provides a rich set of "matchers" to validate different types of data. Knowing which matcher to use makes your tests cleaner and failure messages more helpful.
javascript
Custom Matchers
If you find yourself writing repetitive assertions, you can extend Jest.
javascript
Asynchronous Testing
JavaScript relies heavily on Promises and async/await. Jest handles async code seamlessly.
Using async/await (Recommended):
javascript
Testing Promise Rejections:
javascript
Common Mistake: Forgetting to use await before expect().resolves or expect().rejects. Without await, the test will pass immediately before the promise settles.
Mocks & Spies
To achieve true unit tests, we must isolate the function we are testing from external dependencies like databases, networks, or random number generators.
Spies
Spies allow you to track how a function was called without replacing its implementation.
javascript
Mocks
Mocks replace the implementation entirely. This is crucial for avoiding real network requests.
javascript
Timers
Testing setTimeout or setInterval manually is slow and flaky. Jest can mock time itself.
javascript
React Component Testing (React Testing Library)
While Jest is the runner, React Testing Library (RTL) is the industry standard for asserting on React components. RTL forces you to test components the way users interact with them, rather than testing implementation details.
tsx
Snapshot Testing
Snapshot tests capture the rendered output of a component and save it to a file. On subsequent runs, Jest compares the new output to the saved snapshot. If they differ, the test fails.
javascript
Best Practices for Snapshots:
Keep them small: Giant snapshots of entire pages are impossible to review in Pull Requests.
Treat them like code: Review snapshot changes carefully. Don't blindly run jest -u to update them without understanding why they changed.
Coverage Reports
Running jest --coverage generates an interactive HTML report mapping out your exact code execution paths.
Coverage reports are excellent tools for discovering untested paths (e.g., an if statement you forgot to write a test for), but remember Module 1: coverage measures execution, not assertion quality.
Key Takeaways
Jest is a comprehensive framework combining a runner, assertion library, and mocking.
Extending matchers (like .toBeEven()) can make domain-specific tests much cleaner.
Always remember to use await with expect().resolves or expect().rejects.
Use React Testing Library to test components based on accessibility and user behavior, not internal state.
Keep snapshots small and treat them like real code during code reviews.
Knowledge Check
A junior developer writes a Jest test block containing three tests that verify the logic of an API client. The first test uses jest.spyOn(client, 'fetchData').mockResolvedValue('data'). The developer notices that the second and third tests are randomly failing because they seem to be receiving the mocked data from the first test instead of executing their own logic. What is the most robust way to prevent this test pollution?
You are testing a React LoginForm component using React Testing Library (RTL). You need to simulate a user typing their email address into an input field defined as <input type="email" placeholder="Enter email" className="form-input" />. According to RTL best practices, which selector should you use to find this input?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Question 3: An engineer writes the following test to verify that an asynchronous validation function properly rejects invalid data:
javascript
The test passes. Later, someone accidentally changes the application code so that validateEmail silently returns true instead of throwing an error. Shockingly, the test still passes! Why did this false positive occur, and how do you fix it?
A) The toThrow matcher only works on synchronous functions. The fix is to change it to .rejects.toBe('Invalid format').
B) The test function is missing the await keyword before expect(). Without it, the test finishes executing synchronously and passes before the Promise even resolves or rejects. The fix is to add async to the test callback and await expect(...).
C) Jest automatically swallows unhandled promise rejections in test environments. The fix is to wrap the expectation in a try/catch block.
D) The string matcher 'Invalid format' is too strict and should be replaced with a regular expression /Invalid format/i.
Reveal Answer
Correct Answer: B
This is one of the most common and dangerous mistakes in Jest. When you use .resolves or .rejects, the expect statement itself returns a Promise. If you do not await that Promise (or return it), the test function reaches its end synchronously. Jest assumes that if a test function completes without throwing an error, the test passes. Therefore, the test passes immediately, long before the validateEmail promise actually settles, creating a massive false positive.
You now have a deep understanding of Jest for unit testing and frontend validation. But backend environments have their own challenges. Next, we will explore Backend Testing strategies including API validation and Database mocking.
npminstall --save-dev jest typescript ts-jest @types/jest
npx ts-jest config:init
/** @type{import('ts-jest').JestConfigWithTsJest} */module.exports={preset:'ts-jest',testEnvironment:'node',// use 'jsdom' for React frontendclearMocks:true,// Automatically clear mock calls between testscoverageDirectory:'coverage',collectCoverageFrom:['src/**/*.{js,ts,jsx,tsx}','!src/**/*.d.ts',],};
expect.extend({toBeEven(received){const pass = received %2===0;return{message:()=>`expected ${received} to be an even number`, pass,};},});expect(4).toBeEven();
it('fetches user data successfully',async()=>{const data =awaitfetchUser(1);expect(data.name).toBe('Alice');});
it('throws an error for invalid IDs',async()=>{awaitexpect(fetchUser(-1)).rejects.toThrow('User not found');});
const user ={getAge:()=>25};const spy = jest.spyOn(user,'getAge');console.log(user.getAge());// Still returns 25expect(spy).toHaveBeenCalledTimes(1);
// Mocking an entire modulejest.mock('./api');import{ fetchUser }from'./api';it('mocks an API response',async()=>{// Replace implementation fetchUser.mockResolvedValue({id:1,name:'Mocked User'});const user =awaitfetchUser(1);expect(user.name).toBe('Mocked User');});
jest.useFakeTimers();it('runs the callback after 1 second',()=>{const callback = jest.fn();setTimeout(callback,1000);expect(callback).not.toHaveBeenCalled(); jest.advanceTimersByTime(1000);// Fast-forward timeexpect(callback).toHaveBeenCalledTimes(1);});
import{ render, screen, fireEvent }from'@testing-library/react';import{LoginForm}from'./LoginForm';it('allows a user to log in',()=>{const handleLogin = jest.fn();render(<LoginFormonLogin={handleLogin}/>);// Find elements by accessible rolesconst emailInput = screen.getByRole('textbox',{ name:/email/i});const submitButton = screen.getByRole('button',{ name:/submit/i});// Act: Simulate user behavior fireEvent.change(emailInput,{ target:{ value:'user@test.com'}}); fireEvent.click(submitButton);// Assertexpect(handleLogin).toHaveBeenCalledWith('user@test.com');});
importrendererfrom'react-test-renderer';import{Button}from'./Button';it('renders correctly',()=>{const tree = renderer.create(<Button label="Click Me"/>).toJSON();expect(tree).toMatchSnapshot();});