DEV Community

Cover image for How build testable React components?
Alejandro García Serna
Alejandro García Serna

Posted on

How build testable React components?

Through the years software industry has created many patterns and practices that make the software we write both more flexible and easy to test and that includes our React components, with this in mind let's begin describing the problem.

Note that these examples were written using Typescript since use types and interfaces make easier to see the concept, but you can write it in plain Javascript as well.

Imagine that you have code like this:

userClient.ts

class UserClient {
  getAll(): Promise<User[]> {
    // ... get users fetching them from an API
  }
}

UserList.tsx

const userClient = new UserClient();

const UserList: React.FC = () => {
  const [users, setUsers] = React.useState<User[]>([]);

  React.useEffect(() => {
   userClient.getAll().then((usersList) => setUsers(usersList));
  }, []);

  return (
   <ul>
    {users.map(user => (
     <li key={user.id}>{user.name}</li>
    ))}
   </ul>
  )
}

Here we have a problem since testability perspective, as we are using a concrete instance of UserClient every time we run our UserList tests, we will be making a petition to fetch our users which for a test is not a good idea because your tests should be able to run even if your network connection is down or if the service where the data comes from is failing, so how do we solve this?

With this in mind we'll introduce the first important concept which it's Dependency injection

Dependency Injection

Dependency injection is a pattern where you move the creation of the objects we depend on outside our current class/function/component where this object is called dependency, so, how does this looks like?

UserList.tsx

interface Props {
  userClient: UserClient;
}

const UserList: React.FC<Props> = ({ userClient }) => {
  const [users, setUsers] = React.useState<User[]>([]);

  React.useEffect(() => {
   userClient.getAll().then((usersList) => setUsers(usersList));
  }, []);

  return (
   <ul>
    {users.map(user => (
     <li key={user.id}>{user.name}</li>
    ))}
   </ul>
  )
}

In the interface we defined for our component, we said that we will be receiving a prop called userClient of type UserClient, in this way we derived the responsibility to create the concrete object to the context where the component is rendered

<UserList userClient={new UserClient()} />

This is part of the solution because since our tests we begin to think in the possibility to pass into our component a fake userClient instance which help us with our tests but how do we get that?

Dependency Inversion

Right now if take a look in how our component relate with UserClient we would see something like this:
Relationship

We can notice in this diagram that UserList depends on a concrete instance of UserClient, which it means that whatever this instance does it will have impact in our component even if that means to perform http petitions not only in our normal flow but in our test too, for this problem is where dependency inversion comes to help us.

Dependency inversion is a technique which allows us to decouple one object from another one, in this case our component and our userClient and decouple as its name says is reduce coupling, coupling in software engineering is how much one object knows about another one, the common desired is reduce the coupling because this makes our code more flexible, testable and maintainable. And we get this depending in interfaces rather than in concrete implementations.

First we create a interface for our class UserClient
userClient.ts

interface IUserClient {
  getAll(): Promise<User[]>;
}

class UserClient implements IUserClient {
  getAll(): Promise<User[]> {
    // ... get users
  }
}

Doing this our class UserClient begins to depend in how IUserClient interface says that it should looks like, enforcing how our class should looks like we can guarantee that those places where we rely in that interface are gonna behave in a consistent way no matter which object we pass in as long as implements that interface.

UserList.tsx

interface Props {
  userClient: IUserClient;
}

const UserList: React.FC<Props> = ({ userClient }) => {
  const [users, setUsers] = React.useState<User[]>([]);

  React.useEffect(() => {
   userClient.getAll().then((usersList) => setUsers(usersList));
  }, []);

  return (
   <ul>
    {users.map(user => (
     <li key={user.id}>{user.name}</li>
    ))}
   </ul>
  )
}

Unlike in how we did it before, we are relying in the interface IUserClient rather than the concrete implementation that UserClient is, so our relationship looks something like this:

Dependency Inversion

We can see how now we are not depending in our class UserClient anymore but we are relying in objects which has the shape that IUserClient dictates, so in our tests we can do this

UserList.test.tsx

class FakeUserClient implements IUserClient {
  getAll(): Promise<User[]> {
    return Promise.resolve([{}, {} ]) // Array of users
  }
}

test('Tests for UserClient component', () => {
  render(<UserList userClient={new FakeUserClient()} />)
  // Assertions
});

As we can see, we are not depending anymore in UserClient but in IUserClient interface, we can now use in our component any object as long as implements IUserClient, that's why we can in FakeUserClient class simulate our users petition and avoid them in our tests, making our test faster and independent from network conditions.

I hope you enjoyed it <3, please let me know any doubts you have.

Top comments (2)

Collapse
 
ecyrbe profile image
ecyrbe

Hi,

For react coders that want to use functionnal programming style, i would suggest them to just decouple the business logic ( the fetch api in this case) from the component logic.

In this article exemple, you can just declare a function instead of an interface. But if you need to control a lot of business logic (not just a single function) you can then use HOC pattern.

  • withApiFetch(Component) for non tests
  • withFakeData(Component) for tests

The advantages, HOC is pure functionnal programming, uses composition over inheritance and still usefull even with hooks.

You can also unit test the business logic by dividing it in small testable pure functions.

Collapse
 
thenriquedb profile image
Thiago Henrique Domingues

Design patterns is beautiful!