Smarter Jest Tests for React Native
Jest is one of the most famous test runners. Chances are high that as a React developer, you will work with eventually. That’s why it is good to know how to do things like mocking modules, functions and writing more concise tests. We will take a look at jest.mock
, jest.spyOn
and it.each
.
Running tests in your system is usually what is called test runner. Jest is one of the most prominent when it comes to JavaScript / React / React Native projects. It is simple to setup, configure and use. Tests can be well structured and test expectations sound natural.
In my Ultimate React Native Testing Guide on Gumroad I explain how to use mocks and spyOn in a React Native app. Source code is included. Use the code “reime005-medium” to get 70% discount.
Let’s take a look at a simple function, that we will use for show casing tests. The goal behind the exemplary method getYearMonthDay
is to split a given date, given in the string format “YYYY-MM-DD”, into its parts. Those are YYYY (year), MM (month) and DD (day).
type Output = null | YearMonthDay;
/**
* Splits a given date into the YYYY (year) MM (month) DD (day) parts
* @param date Date in the format YYYY-MM-DD
* @returns year, month, day object
*/
export const getYearMonthDay = (date: string | undefined): Output => {
const yearMonthDay = date?.split('-');
if (yearMonthDay?.length !== 3) {
return null;
}
return {
year: yearMonthDay[0],
month: yearMonthDay[1],
day: yearMonthDay[2],
};
};
The delimiter, that separates the three parts, is the dash -
. Once split, the length is being checked, so that, eventually, errors are being caught and null
be returned. Finally, the year, month and day will be returned as an object.
GIVEN-WHEN-THEN Behavioural Pattern
Following a specific test convention is always good. It ensures clarity and structure across the project. The most prominent pattern is called given-then-when. It originates from the Behavior Driven Development (BDD). The idea is that your tests have a clear intent and are concise:
- GIVEN: Your setup or setup for that test
- WHEN: Your action or method that acts on the given data
- THEN: The final evaluation or assertion for the result
Given that pattern, a simple test case for getYearMonthDay
could look like the following. The date “2022–12–12” should be parsed into its three parts.
it('should parse "2022-12-12"', () => {
// GIVEN
const input = '2022-12-12';
// WHEN
const result = getYearMonthDay(input);
// THEN
expect(result).toEqual({ year: '2022', month: '12', day: '12' });
});
This function is a good example for unlimited test cases, due to its input. You should limit your data set for tests to a few useful values. But how do you know what data you should test your functions with? Here are some basic rules that you can use:
- test for undefined / null and Infinity, as well as 0, if necessary
- test for edge cases (really low, really high or close to a critical value like 0)
- test for often occurring or default values
Still, this would mean that we would have to create plenty of tests, that all have the same structure. For that, it.each
can be useful.
Jest it.each to collect tests
Taking into account a variety of different test cases, like covering an empty string, null
, undefined
or malformed dates, we can come up with the following it.each
test:
it.each([
['2012-12-12', { year: '2012', month: '12', day: '12' }],
['2021-12-1', { year: '2021', month: '12', day: '1' }],
['1970-01-01', { year: '1970', month: '01', day: '01' }],
['2021', null],
['', null],
[undefined, null],
])('should parse %s to %s', (input, expected) => {
expect(getYearMonthDay(input)).toEqual(expected);
});
As you see, there is only one written test case, but multiple input data. This makes it more concise and also easier to maintain.
Skipping Jest tests
As a quick tipp: You can skip a Jest test, by writing xit
instead of it
. You could also comment out the whole test, but then it would not appear as skipped.
Mocking in Jest
If you want to test code which uses modules or functions, you can use mocking to alter its implementation or behavior. This makes sense when using libraries, or when testing a specific scenario or behavior. The jest.mock
functionality will be used for mocking modules. This mocks the module with an auto-mocked version when it is being required.
Let’s take another function as an example for mocking in tests. The function normalizeNaturalData
takes an array of data retrieved from an API. The output should be list of image URIs, generated from that array.
/**
* Filters the data retrieved by the natural API and returns
* the needed image URIs.
*
* @param input Date we get from the API
* @returns image URIs
*/
export const normalizeNaturalData = (
input: Array<{ date: string; image: string }>
): string[] => {
const filterData = (date: string | null): date is string =>
typeof date === "string";
return input
.map((d) => {
const date = getDate({ dateAndTime: d.date });
if (!date) {
return null;
}
return getImagePath({ ...date, image: d.image });
})
.filter(filterData);
};
The function uses a combination of map
and filter
to generate the return. A method called getDate
is also being shown, which will be the mocked part here. It is a simple helper function that should return the date part of the ISO-8601 date-time format.
The getImagePath
function exists mostly for test purposes and should return the image uri for a given day. For that, we need the year, month, day and an image name.
Mocking a module
A module can be mocked using the jest.mock
function. It works in a way that it auto-replaces the target module with an auto-mocked version, when it is being required. This means you can call the mock before/after imports, or globally via a setup file. Latter is usually done for external libraries. A local mocking of a module can look like the following:
import { getDate } from 'utils/getDate';
jest.mock('utils/getDate');
A test case for the getDate
function would look like following:
it('should mock getDate so that "2019-01-12" should be returned as a date' , () => {
(getDate as jest.Mock).mockReturnValue({
day: '12',
month: '01',
year: '2019',
});
const result = normalizeNaturalData([{ image: '42' }]);
expect(result).toEqual([
'https://some.uri',
]);
});
The mockReturnValue
function returns the given value every time the mocked function is called. You could also use mockReturnValueOnce
, which helps with returning different values and supports function chaining.
You should make sure, that the mock is being cleared after the test run(s). This can be done via the clearAllMocks
function, for example. The Jest lifecycle method afterEach
can be used, so that after each test run, the mocks are cleared.
afterEach(() => jest.clearAllMocks());
Furthermore, you could create specific mock files, called manual mocks. They are located in the module’s subdirectory, named __mocks__/
.
Jest spyOn
You can create a specific mock function, using jest.fn
, or use the jest.spyOn
function, which returns a Jest mock function. With spyOn, you can track calls to the specific function of that module.
Taken the example getDate
function, we can spy on the getYearMonthDay
function. The following test case demonstrates the use of jest.spyOn
and the toHaveBeenCalledWith
assertion:
it.each([
['', null],
['2012-12-9', null],
['2012-12-9 1992', { year: '2012', month: '12', day: '9' }],
['2012-12-9 12:30:00', { year: '2012', month: '12', day: '9' }],
])('should parse %s to %s', (input, expected) => {
const spy = jest.spyOn(getYearMonthDay, 'getYearMonthDay');
const result = getDate({ dateAndTime: input });
expect(result).toEqual(expected);
const dateAndTimeSplits = input.split(' ');
// example of how to use the jest.spyOn method
if (dateAndTimeSplits.length === 2) {
expect(spy).toHaveBeenCalledWith(dateAndTimeSplits[0]);
}
});
Using toHaveBeenCalledWith
, you can check whether the spied on function has been called with the expected argument. Furthermore, toHaveBeenCalledTimes
can sometimes be useful, to test how often the function has been called.