Hi developers,
React developers love crafting components, but when it comes to code coverage, they often find themselves at a loss. This blog aims to change that! By the end, you’ll feel confident in writing test cases that minimize bugs.
I’ve shared some key points and techniques below to help you write React components that are easy to test.
- Before writing any component, ask yourself: Is it testable? If the answer is no, it’s time to change your approach.
Solution: Embrace the TDD (Test-Driven Development) approach.
- When mocking, follow the principle of abstraction by defining the implementation.
Instead of passing state and handlers as props, and to avoid mocking API calls, define the implementation by passing methods as props. This allows you to create mock responses effectively.
Major of the times ,when we write the component either we have to mock these -
- Any of the package/file's methods or properties
- Any hooks
- Any type of network call
But magic only reside focus on imported package ,define to mock their function or property and then define implementation.
If it function return promise use mockResolve , if multiple function getting call the define called in stack using mockResolveOnce.
Lets see some of the approaches by using this we can write testable components.
Using MockAdapter -
First make sure you have installed this package - 'axios-mock-adapter' if not use command
yarn add axios-mock-adapter
After that you can mock network like -
const fetchedData = {}
describe('searchData', () => {
let mockAxios: MockAdapter;
beforeEach(() => {
mockAxios = new MockAdapter(axios);
});
afterEach(() => {
mockAxios.reset();
mockAxios.restore();
});
it('should fetch all brands', async () => {
const keyWord: string = 'paint';
let mockAxios: MockAdapter = new MockAdapter(axios);
const data = [];
const searchApi: string = `apiUrl?keyWord=${keyWord}`;
mockAxios.onGet(searchApi).reply(200, JSON.stringify(fetchedData));
const result = await searchData('paint');
expect(result).toEqual(data);
expect(mockAxios.history.get.length).toBe(1);
expect(mockAxios.history.get[0].url).toBe(searchApi);
});
});
How to mock function from particular method ?
Suppose you have one file like getChangeHistory.ts -
const getChangeHistory = async (
promotionId: string,
fetchAll: boolean = false,
): Promise<ApiResponse> => {
const baseUrl: string = `apiUrl`;
const params: string = fetchAll ? 'fetch=ALL' : '';
const changeHistoryApiUrl: string = `${baseUrl}?${params}`;
const axiosInstance: AxiosInstance = getAxiosInstance('apiUrl');
const response: AxiosResponse = await axiosInstance.get('changeHistoryApiUrl');
return response.data;
};
In above code we will try to mock getAxiosInstance method and provide our own implementation because this method is responsible for api call -
jest.mock('path of the file where getAxiosInstance is define', () => ({
getAxiosInstance: jest.fn(),
}));
// example : '../../../../../src/lib/api/config'
Now your test case will look like -
describe('getChangeHistory', () => {
let mockedAxiosInstance: jest.Mocked<any>;
const offerId: string = '1234567890';
beforeEach(() => {
mockedAxiosInstance = getAxiosInstance as jest.Mock;
});
it('should fetch offer history for offer', async () => {
const offerHistory = {
changeHistory: [
],
};
// defining implementation
mockedAxiosInstance.mockReturnValue({
get: jest.fn().mockResolvedValue({ data: fetchOfferHistory }),
});
const result = await getChangeHistory(id);
//checking assertion / interactions
expect(result).toEqual(offerHistory);
expect(getAxiosInstance).toHaveBeenCalledWith(TEST_USER, 'apiUrl');
});
it('should handle API errors gracefully', async () => {
mockedAxiosInstance.mockReturnValue({
get: jest.fn().mockRejectedValue(new Error('Request failed with status code 500')),
});
await expect(getChangeHistory(TEST_USER, id)).rejects.toThrow(
'Request failed with status code 500',
);
});
});
Lets see how can we mock any method present in file or package.
I am taking example of axios which is having it owns instance
jest.mock('axios');
const mockAxios = axios as jest.Mocked<any>;
And define its implementation like
const fetchedBrands = () =>{}
mockAxios.create.mockImplementation(() => mockAxios);
mockAxios.get.mockResolvedValue({ data: fetchedBrands });
Here create and get are the methods which is present in AxiosInstance . We have provided mock implementation using mockResolvedValue where fetchedBrands is mocked data.
Hence your test file look like -
jest.mock('axios');
const mockAxios = axios as jest.Mocked<any>;
const mockedData = {}
describe('Test : Search data API', () => {
it('Should search data successfully', async () => {
mockAxios.create.mockImplementation(() => mockAxios);
mockAxios.get.mockResolvedValue({ data: fetchedBrands });
const dataKeyWord = 'vers';
const receivedResponse = await searchData(dataKeyWord);
expect(receivedResponse).toStrictEqual(mockedData);
expect(axios.get).toHaveBeenCalledWith(apiUrl);
});
});
Actually seachData method is look like -
export const searchBrands = async (user: User, brandName: string) => {
const params: string = new URLSearchParams({ brandName }).toString();
const searchBrandsApiUrl: string = 'apiUrl;
const apiInstance: AxiosInstance = getAxiosInstance();
const response: AxiosResponse = await apiInstance.get(searchBrandsApiUrl);
return response.data;
};
Now lets see how can we make any function from any file / package and define implementation
Like same for package -
jest.mock('@my-package-name/folder', () => ({
fooFunction: jest.fn(),
}));
Lets define the mock implementation -
let mockfooFunction: jest.MockedFunction<any>;
beforeEach(() => {
jest.mock('@my-package-name/folder', () => ({
fooFunction: jest.fn(),
}));
mockGetFooFunction = fooFunction as jest.MockedFunction<
typeof fooFunction
>;
});
Your test case will look like -
describe('Test Cases', () => {
let mockFooFunction: jest.MockedFunction<any>;
beforeEach(() => {
jest.mock('@my-package-name/folder', () => ({
fooFunction: jest.fn(),
}));
mockFooFunction= fooFuction as jest.MockedFunction<
typeof fooFunction
>;
});
it('Should do something', async () => {
mockFooFunction.mockResolvedValue(mockedStoreData);
/* fooFunction will be called inside MyLayout. It is a network call to fetch data available inside '@my-package-name/folder' */
render(
<MyLayout/>,
);
});
Loading....
Notes -
To improve your tests, ensure they are readable and well-structured. Here are some key points:
Separate Rendered and Event-Based Tests: Write tests for rendering and events separately to maintain clarity.
Use Mock Data Helper Functions: Create helper functions for mock data to keep your tests clean and reusable.
Name Tests Clearly: Follow the “Should be” convention for test case names to make their purpose clear.
Use act with await: For interactive tests, such as setting a text box value or clicking a button, use act with await to handle asynchronous updates.
Isolate Components: Ensure each component is isolated from others to avoid dependencies and side effects.
Focus on Integration Testing: Prioritize integration tests that cover interactions between all the tiny components to ensure comprehensive coverage.
Show me your love ❤️
Top comments (0)