Best Practices for Testing with Jest - part 1
Writing tests is an essential part of maintaining reliable and scalable applications. With Jest being one of the most popular testing frameworks in the JavaScript ecosystem, it's crucial to understand the best practices that can help you write clean, efficient, and maintainable tests.
In this post, we’ll explore some of the best practices for writing tests with Jest, focusing on test structure, naming conventions, isolation, readability, and ensuring tests reflect real-world user behavior.
By following these guidelines, you can enhance the quality of your tests and ensure that your code performs as expected, even as your application evolves. Let’s dive in!
1 - Test Structure: Write Tests in AAA Style
The Arrange-Act-Assert (AAA) pattern keeps your tests clear and structured:
Example:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import MyComponent from './MyComponent';
test('should update input value', async () => {
// Arrange
render(<MyComponent />);
const input = screen.getByRole('textbox');
// Act
await userEvent.type(input, 'Hello, Jest!');
// Assert
expect(input).toHaveValue('Hello, Jest!');
});
2 - Descriptive test names and describe blocks
Good test names act as documentation and make it easy to understand what is being tested.
Example:
describe('User Login', () => {
test('should allow a user to log in with valid credentials', () => {
// test logic
});
test('should show an error message for incorrect credentials', () => {
// test logic
});
});
3 - Independence: Keep Tests Isolated
Example:
Wrong ?:
import Counter from './Counter';
describe('Counter', () => {
let counter;
// First test: rely on previous state
test('should increment value', () => {
counter = new Counter();
counter.increment();
expect(counter.getValue()).toBe(1);
});
// Second test: this depends on the first test, which is problematic
test('should increment value again', () => {
expect(counter.getValue()).toBe(2); // This assumes the first test already ran successfully
});
});
Right ?:
import Counter from './Counter';
describe('Counter', () => {
// First test: isolated
test('should increment value', () => {
const counter = new Counter(); // Initialize inside the test
counter.increment();
expect(counter.getValue()).toBe(1);
});
// Second test: isolated
test('should increment value again', () => {
const counter = new Counter(); // Initialize again
counter.increment();
counter.increment();
expect(counter.getValue()).toBe(2);
});
});
4 - Cleanup (Teardown) after each test:
领英推荐
Example:
// resets mocks after each test
afterEach(() => {
jest.clearAllMocks();
});
5 - Avoid getByTestId – Don’t tie tests to implementation
Using getByTestId can make your tests too dependent on internal details, leading to brittle tests when refactoring.
When you use getByTestId, the test depends on a specific data-testid attribute in the component's code. This means that if the name or structure of the data-testid changes during refactoring or updates to the code, the tests will break even if the functionality of the application remains unchanged.
getByTestId accesses elements based on internal implementation details. In contrast, testing with methods like getByRole, getByLabelText, or getByText focuses on how the user interacts with the UI rather than the internal details.
For example, a user doesn't care about the data-testid attribute of a button. They care about the button's name or its function, such as "Submit" or "Send." Using getByRole("button", { name: /submit/i }) better reflects actual user behavior and ensures that the test is resilient to changes in the internal implementation.
Prefer using getByRole, getByLabelText, getByText, or getByPlaceholderText to query elements in the DOM. These methods focus on how users interact with the elements, making the test more resilient to changes in the implementation.
? Prefer role-based queries:
const button = screen.getByRole('button', { name: 'Submit' });
? Avoid:
const button = screen.getByTestId('submit-button');
6 - Why use userEvent instead of fireEvent?
When testing React components, it's essential to simulate user interactions in a way that reflects how users interact with the app in real life. While both userEvent and fireEvent allow you to trigger events in tests, userEvent is often preferred because it provides a more realistic simulation of actual user behavior.
Example Comparison:
fireEvent.change(inputElement, { target: { value: 'Hello' } });
This only triggers the change event and doesn't consider typing speed or focus.
await userEvent.type(input, 'Hello, world!');
This makes tests more reliable and realistic compared to fireEvent.input(input, { target: { value: 'Hello' } }).
?? Conclusion
By following these best practices, you'll write clean, maintainable, and effective tests with Jest. Good testing leads to fewer bugs, more confidence in deployments, and a better development experience. ??
What are your go-to Jest best practices? Let’s discuss in the comments! ??
Fullstack Software Engineer | Node | Typescript | React | Next.js | AWS | Tailwind | NestJS | TDD | Docker
3 周Well said and thanks for sharing! Lucas Mendon?a
Senior React Developer | Full Stack Developer | JavaScript | TypeScript | Node.js
3 周Nice, thanks for sharing !
Android Developer | Mobile Software Engineer | Kotlin | Jetpack Compose | XML
3 周Well done!!
Senior Full Stack Software Engineer | C# | .NET | SQL | Javascript | React | JQuery | TDD
3 周Great post, I am a unit test enthusiast as well, and I believe this same concept goes beyond Jest and JavaScript, and is valid for any stack.
Senior Business Analyst | ITIL | Communication | Problem-Solving | Critical Thinking | Data Analysis and Visualization | Documentation | BPM | Time Management | Agile | Jira | Requirements Gathering | Scrum
3 周Insightful! Thanks for sharing Lucas! ????