Unit Testing in React using Enzyme and Jest
In software development, unit testing means checking individual code components or functions to ensure the code executes correctly without error in all possible scenarios before integrating them into the larger application. This practice make the code reliable and improves code quality. Lastly, it identifies bugs early, and makes development more efficient.
Why do we consider unit testing React components crucial?
Unit testing React components is crucial for several reasons, especially for beginners:
- Detect Bugs Early: Unit tests assist in catching errors and issues in your components as soon as you write them. Hence, you can fix bugs/issues before they become big, complex and hard to track down.
- Isolation: Multiple React components bundled together to create a web page or application. Unit tests allow you to test each component individually, ensuring that each component function correctly in isolation.
- Confidence in Changes: When you make changes or updates to your existing components or add new child components, unit tests act as a safety pillar. They notify you if your changes break existing functionality, providing confidence that your changes didn’t introduce new issues. Moreover, team members can confidently make changes to the codebase, knowing that tests will catch issues if something breaks.
- Documentation: Test cases act as description how your components should behave, making it easier for you or others to understand and maintain the code.
- Improves Code Quality: Following the practice of writing tests encourages you to write cleaner, more modular code. It forces you to think about how your components should behave and how they interact with data and other components.
- Saves Time: While it may seem like extra work, unit tests can save you time in the long run.
- Continuous Integration (CI): Many development workflows include CI, where code is automatically tested before being merged into the main codebase. Unit tests ensures that only functioning code is integrated.
Structure of a Unit Test:
- Test Setup: All the necessary setup before running the test are required like importing the module or function you want to test, setting up any dependencies or mocks, configuration specific to the testing framework (e.g., Enzyme adapter configuration for React component tests).
- Test Execution: In this part, you actually run the test. This often involves calling a function or method from the module being tested with specific inputs or scenarios.
- Assertions: After executing the test, you use assertions to verify the expected outcomes. Assertions are statements that express what you expect to be true about the tested code. Common assertions include comparing values, checking for exceptions, or verifying that specific functions were called with certain arguments.
- Cleanup (Optional): If your test has any side effects that need to be cleaned up after execution, this is where you would do it.
Prerequisites
To proceed with the blog, you should have the basic knowledge of JavaScript and React. Also, Enzyme and Jest should be installed in the React project.
Lets write out first simple React Component
import React, { Component } from "react";
import RecordHeader from "./RecordHeader";
import RecordDetail from "./RecordDetail";
import { forEach, groupBy } from 'lodash'
import Axios from "axios";
class RecordProcess extends Component {
constructor() {
super();
this.state = {
records:[]
};
}
componentDidMount() {
let id = "EA1011";
if(id){
Axios.get('URL?id='+id)
.then((response) => {
if(response && response.data && response.data.length){
this.setState({ records: response.data })
}
})
}
}
render() {
return (
<div className="record-layout">
<RecordHeader />
<RecordDetail recordLines={this.state.records}
groupedIb={this.state.groupedIb}/>
</div>
);
}
}
export default RecordProcess;
Lets write out Unit Test Cases for this component
import React from 'react';
import {mount} from 'enzyme';
import RecordProcess from '../components/Record/RecordProcess';
import Adapter from "enzyme-adapter-react-16";
import "jest-enzyme";
import axios from "axios";
jest.mock("axios");
configure({ adapter: new Adapter() });
describe("RecordProcess", ()=>{
it('+ render the Record Parent component', ()=>{
const recordData = [
{ listPrice: 10, qty: 2, checked: true },
{ listPrice: 20, uqty: 3, checked: true },
];
const propsMocked = {
records: recordData
}
let component = mount(<RecordProcess {...propsMocked}/>),
instance = component.instance();
expect(component.length).toEqual(1)
});
});
Here’s an explanation of each part of the code:
Imports: Importing the necessary modules and libraries for testing:
- React: The React library.
- RecordProcess: The React component you want to test (imported from its file).
- Adapter from “enzyme-adapter-react-16”: The Enzyme adapter for React 16.
- “jest-enzyme”: An extension for Jest to simplify assertions with Enzyme.
- axios: The Axios library for making HTTP requests.
- jest.mock(“axios”): Mocking the Axios library to intercept HTTP requests and simulate responses.
Enzyme Configuration: Configuring Enzyme to use the React 16 adapter.
Test Suite: Creating a test suite using describe
to group related test cases. In this case, it's named "RecordProcess."
Test Case: Writing a test case using it
. The test is named "+ render the Record Parent component," which suggests it's checking if the component renders successfully.
Test Data: Creating sample data (recordData
) that you intend to pass as props to the RecordProcess
component. This data simulates the expected props for testing.
Props Mocking: Creating mocked props (propsMocked
) with the sample data.
Component Rendering: Mounting (rendering) the RecordProcess
component using Enzyme's mount
function and providing the mocked props.
Instance: Getting an instance of the mounted component (though it’s not used in this test case).
Assertion: Using expect
to assert that the length of the component
is equal to 1. This checks if the component was successfully rendered. If it wasn't, the length would be 0.
This code demonstrates a basic unit test for rendering a React component (RecordProcess
) with specific props. It's useful for ensuring that the component renders correctly and can serve as a starting point for more extensive testing of your React components.
Introduction to Jest
Jest is a powerful and widely-used JavaScript testing framework that plays a crucial role in ensuring the reliability and functionality of JavaScript and React applications. Whether you’re a seasoned developer or just getting started with testing, Jest provides an accessible and efficient way to validate the behavior of your code. In this introduction, we’ll explore what Jest is, why it’s so popular, and how it simplifies the process of testing your codebase.
What is Jest?
Jest, developed by Facebook, is an open-source testing framework that specializes in simplicity and ease of use. It is designed to make testing JavaScript and React applications a seamless experience. At its core, Jest helps you write, run, and manage tests effortlessly, making it an essential tool for developers who want to maintain code quality and catch potential issues early in the development process.
Introduction to Enzyme:
Enzyme is a popular JavaScript testing utility library primarily used for testing React applications and components. It makes it easier to interact with and assert the behavior of React components. Enzyme provides a set of convenient and expressive methods for rendering, querying, inspecting and manipulating React components during tests.
Enzyme is useful for testing React components for several reasons:
- Multiple Rendering Methods: Enzyme offers different rendering methods (e.g., shallow rendering and full rendering) to cater to various testing scenarios, allowing you to control the level of isolation and complexity in your tests.
- DOM Manipulation: It enables you to simulate user interactions (e.g., clicks, input changes) and inspect the resulting changes in the component’s state or UI.
- Assertions: Enzyme integrates seamlessly with other popular testing frameworks like Jest and provides a wide range of assertions, making it easy to check if a component’s output matches your expectations.
- Component Inspection: You can traverse and query the rendered component’s structure, inspecting props, state, and DOM elements to make precise assertions.
Different Rendering Methods in Enzyme:
shallow
Rendering: Shallow rendering is used to render a component without rendering its child components deeply. It creates a “shallow” copy of the component tree. Child components are not fully instantiated or rendered. It’s faster thanmount
because it avoids rendering child components. Example:shallow(<MyComponent />)
mount
Rendering:mount
renders a component and all its child components fully, allowing you to test the component as it would behave in a real DOM environment. Usemount
when you need to test interactions between a component and its children or when you want to test the complete lifecycle of a component. It interacts with the real DOM for testing components with complex interactions. Example:mount(<MyComponent />)
render
Rendering: Therender
method renders a component to static HTML, similar to server-side rendering. It's useful for testing how a component renders to HTML. Testing component snapshots for UI consistency or generating static HTML for server-side rendering. It returns a wrapper containing a virtual DOM representation of the component’s output. Example:render(<MyComponent />)
Choosing the appropriate rendering method in Enzyme depends on your testing goals and the level of isolation you require. Shallow rendering is often sufficient for testing individual component units, while mount
and render
allow for more comprehensive testing of component interactions and rendering.
Testing Components with Enzyme:
import React from 'react';
import {mount} from 'enzyme';
import RecordProcess from '../components/Record/RecordProcess';
import Adapter from "enzyme-adapter-react-16";
import "jest-enzyme";
import axios from "axios";
jest.mock("axios");
configure({ adapter: new Adapter() });
describe("RecordProcess", ()=>{
it('+ renders component with the correct props', () => {
const recordData = [
{ listPrice: 10, qty: 2, checked: true },
{ listPrice: 20, uqty: 3, checked: true },
];
const propsMocked = {
records: recordData
}
let component = mount(<RecordProcess {...propsMocked}/>);
const instance = component.instance();
const compProps = instance.props;
expect(compProps).toEqual(propsMocked);
});
});
it('+ should initialize state correctly', () => {
let component = mount(<RecordProcess />);
const instance = component.instance();
expect(instance.state).toEqual({
records: []
});
});
it('+ fetches API data and updates state correctly in componentDidMount', async () => {
const mockResponseData = [
{ listPrice: 10, qty: 2, checked: true },
{ listPrice: 20, uqty: 3, checked: true },
];
axios.get.mockResolvedValue(Promise.resolve(mockResponseData));
let wrapper;
await act(async () => {
wrapper = shallow(<RecordProcess />);
});
await wrapper.update();// Wait for the Axios request to complete (async)
expect(axios.get).toHaveBeenCalledWith('URL?id=EA1011');
expect(wrapper.state('records')).toEqual(mockResponseData);
})
Some key points to be explained
- Use
act
for Asynchronous Testing: You use theact
function to wrap the rendering of the RecordProcess component. This is because the component'scomponentDidMount
method contains an asynchronous operation (the Axios API request). Wrapping it inact
ensures that the component's lifecycle methods are properly handled in the testing environment. - Wait for Component Update: After rendering the component, you call
await wrapper.update()
to wait for any pending asynchronous tasks to complete. This is crucial to ensure that the Axios request has time to finish and update the component's state. - Expect Axios Request to Be Called: You use
expect(axios.get).toHaveBeenCalledWith('URL?id=EA1011')
to assert that the Axiosget
method was called with the expected URL and query parameters. This verifies that the component is making the API request as intended.
Suppose add a function to the previously defined React Component
import React, { Component } from "react";
import RecordHeader from "./RecordHeader";
import RecordDetail from "./RecordDetail";
import { forEach, groupBy } from 'lodash'
import Axios from "axios";
class RecordProcess extends Component {
constructor() {
super();
this.state = {
records:[]
};
}
calTotalRecordsValue = () => {
let totalRecordValue = 0;
if(this.state.records && this.state.records.length){
this.state.iblines.forEach((ib) => {
totalRecordValue += ib.listPrice * ib.ibQuantity ;
});
}
return totalRecordValue;
}
componentDidMount() {
let id = "EA1011";
if(id){
Axios.get('URL?id='+id)
.then((response) => {
if(response && response.data && response.data.length){
this.setState({ records: response.data })
}
})
}
}
render() {
const totalRecordVal = this.calTotalRecordsValue();
return (
<div className="record-layout">
<RecordHeader totalRecordVal={totalRecordVal}/>
<RecordDetail recordLines={this.state.records}
groupedIb={this.state.groupedIb}/>
</div>
);
}
}
export default RecordProcess;
it('+ calculates full conversion value with default percentage and no iblines - calFullConversionValInHeader', () => {
const mockResponseData = [
{ listPrice: 10, qty: 2, checked: true },
{ listPrice: 20, uqty: 3, checked: true },
];
const wrapper = shallow(<RecordProcess />);
const instance = wrapper.instance();
wrapper.setState({records: mockResponseData})
const result = instance.calFullConversionValInHeader();
expect(result).toBe(80);
});
Best practices for writing unit tests
1. Follow a Consistent Naming Convention: Use descriptive names for your test suites and test cases. Clear and concise names make it easier to understand the purpose of each test.
2. Keep Tests Isolated: Each unit test should focus on testing a specific piece of functionality in isolation. Use mocking and stubbing to isolate the code being tested from external dependencies such as databases or APIs.
3. Arrange, Act, and Assert (AAA): Organize your test cases into three sections: Arrange, Act, and Assert.
- Arrange: Set up the initial conditions and any necessary objects or data.
- Act: Execute the code being tested.
- Assert: Verify the expected outcomes.
4. Use Meaningful Test Data: Choose test data that covers various scenarios, including edge cases, boundary values, and typical use cases.
5. Avoid Global State Changes: Be cautious about modifying global or shared state in your tests, as it can lead to unintended side effects. Isolate and reset state as needed to prevent interference between tests.
6. Focus on Behavior, Not Implementation: Test the behavior of the code rather than its internal implementation details. This makes tests more robust to changes in the implementation.
7. Write Small, Focused Tests: Keep test cases concise and focused on testing a single aspect of functionality. Avoid creating overly large or complex test cases that are hard to understand.
8. Prioritize Test Coverage: Aim for good test coverage to ensure that critical code paths are tested. Tools like code coverage reports can help identify areas that need additional testing.
9. Document Your Tests: Add comments or annotations to your test cases to explain the purpose and expected outcomes. Well-documented tests are easier for others to understand.
10. Organize Test Files and Suites: Maintain a clear directory structure for your test files, mirroring the structure of your source code. Group related tests into test suites or describe blocks based on the functionality being tested.
By following these best practices, you can ensure that your unit tests are effective, maintainable, and a valuable part of your software development process. Well-written tests provide confidence in code quality, facilitate collaboration among team members, and make it easier to detect and fix issues early in the development process.