This is a pretty simple blog post tackling a problem I’ve seen a few times on React projects. There are quite a few ways of tackling it (Redux for example) but this is a simple, no dependency option. I’m also going to point out what should be obvious, the code here is just example code and is missing lots of things production code would have. All that aside, on to what the problem actually is!

The Problem

Say you’re building a React component that needs to fetch data from some API and then you need to display the data. Our simple example will be an API that has to fetch users and display their usernames in a list.

Let’s say you’ve put the fetching of users into a function that looks something like this:

export async getUsernames() Promise<string[]> {
	const response = await fetch("/api/users");
	if (response.ok) {
		throw new Error("Unable to fetch users");
	}

	return response.json();
}

And now this is one way that you could implement a class which displays this username list:

import React, { FunctionComponent, useEffect, useState } from "react";

import { getUsernames } from "./getUsernames";

const UsersList: FunctionComponent = () => {
	const [users, setUsers] = useState<undefined | string[]>(undefined);

	useEffect((): void => {
    	getUsernames().then(setUsers);
    }, [setUsers]);

	return (
    	<ul>
        	{users === undefined ? "Loading..." : users.map((user) => <li>{user}</li>)}
        </ul>
    );
};

export default UsersList;

Note that at the time of writing the Suspense API is still experimental which is why it’s not used.

Now how would you go about writing a test for this component?

I suppose you could let the component hit a real implementation of the API but that’s going to be annoying because then you either need to provide access to a deployed version of the service to all developers doing local development or you must run the API locally which is just one more thing to run while working on a UI. There’s a good chance will all of these options that things will get more flaky and complicated to maintain.

You could also implement a mock HTTP server which provides an API that looks the same which makes your tests less likely to be flaky but now you also need to be aware of how the API works within every component that happens to use the getUsernames function or any component that consumes it. That’s a lot of layers of abstraction to break.

An even simpler approach would be to mock out the fetch call but that tends to lead to either more code like jest mocks or mocking out global values which creates interdependence between tests and also tends to look pretty hairy.

My solution

The approach I’ve landed on which I think works really well is to simply pass the getUsernames function as a prop into the component. That would look something like this:

import React, { FunctionComponent, useEffect, useState } from "react";

import { getUsernames as realGetUsernames } from "./getUsernames";

interface Props {
	getUsernames: typeof realGetUsernames,
}

const UsersList: FunctionComponent<Props> = ({
	getUsernames: realGetUsernames,
}: Props) => {
	const [users, setUsers] = useState<undefined | string[]>(undefined);

	useEffect((): void => {
    	getUsernames().then(setUsers);
    }, [setUsers]);

	return (
    	<ul>
        	{users === undefined ? "Loading..." : users.map((user) => <li>{user}</li>)}
        </ul>
    );
};

export default UsersList;

Now if you were writing a test all you have to do is provide an implementation of the getUsernames function that behaves how you like.

// An implementation which resolves to values.
const mockGetUsernames: typeof getUsernames = (): Promise<string[]> => {
	return Promise.resolve([
    	"username1",
        "username2",
        "username3",
    ]);
};

// An implementation which returns an error because something happened.
const mockGetUsernames: typeof getUsernames = (): Promise<string[]> => {
	return Promise.reject("Something bad happened");
};

With this strategy testing becomes so much easier and you know longer need to be aware of how all the different API’s and external resources your application needs work for each test.