Improving testability of your react components

Improving testability of your react components

·

10 min read

Featured on Hashnode

If you ask any developer about tests, they answer that tests are essential. They indicate that the code works as intended and that your new change didn’t break something else. However, if you go into almost any React project, you can notice that their tests are not great. Many of them have a vast amount of snapshot tests and maybe some end-to-end tests. There are no proper unit tests and no event testing. So why is that? My opinion is on the way components are built. They are too large and have too much logic inside. And in this post, I am explaining how I think you should structure components to test them.

Alt Text

Why aren’t your components testable?

Before explaining how to structure your component, let’s cover two crucial things that make them not easily testable. And those are JavaScript scoping and not using pure functions.

JavaScript scope of the definition

When discussing the scope of definition, I am talking about areas in the code where your variable or function is visible. In JavaScript, we have a function scope. That means everything defined in a function is visible in that function but not outside of it. Today, we mostly use stateless components in React, and they are functions. Combining that with how JavaScript scope works means anything defined inside of the component is not accessible outside. It also means that you can test the effect of the function defined inside the component, but not the function itself, as it is not visible to your tests. And immediately, it isn’t a proper unit test.

Pure functions

Before understanding why not using the pure function is an issue, you need to understand what pure function is. When looking at the definition, it says there are two requirements for the function to be pure. The first one is that the same arguments give the same result, and the second one is that it doesn’t have side effects. So what does that mean? const name = “John”

function greeting() {
    return `Hello, ${name}`;
}

If we look at the example above, this function is not pure as it breaks the first rule. The name used for a greeting is defined outside of the function and not passed as a parameter. That means function might return different results for different runs depending on the value of some external variable. If you would test this function, you need to define the value of that external variable first. And hope something doesn’t override it. It is something that is often happening in React components as many use props like this. But we could fix this by passing the name as a function argument, and with it would become a pure function. const name = “John”

function greeting(personName) {
    return `Hello, ${personName}`;
}

greeting(name);

The second requirement is a bit less often today. It happens when your function tries to change the value of variables outside of its scope. Using the previous example would be having a greeting value variable modified inside of the function.

const name = “John”
let greetingText;

function greeting(personName) {
    greetingText = `Hello, ${personName}`;
}

greeting(name);

You can fix this by making the function return greeting value instead change it inside.

const name = “John”

function greeting(personName) {
    return `Hello, ${personName}`;
}

let greetingText = greeting(name)

Making component testable

Exclude in service

Now we can cover how to make components testable. And for that, I am starting with a simple, already made component. All this component has is an input field and a div that shows all numbers stripped from that text.

Alt Text

If you look at the code below, it is not a complex component. Two functions. One for handling change even and one for removing numbers from the string. But how would you test that function?

function DemoApp() {
    const [value, setValue] = useState("");
    const [cleanValue, setCleanValue] = useState("");

    function stripNumbers(text) {
        return text.replace(/\d+/g, "");
    }

    function handleChange(ev) {
        const newValue = ev.target.value;
        setValue(newValue);
        setCleanValue(stripNumbers(newValue));
    }

    return (
        <>
            <div>
                <input value={value} onChange={handleChange}/>
            </div>
            <div>{cleanValue}</div>
        </>
    )
}

You can render the component, trigger change events on input and then test that div’s content. It is not a unit test. And you can’t test it on its own as it is a private function. A better option would be to exclude the function into a separate service file.

import stripNumbers from "./stripNumbers";

function DemoApp() {
    const [value, setValue] = useState("");
    const [cleanValue, setCleanValue] = useState("");

    function handleChange(ev) {
        const newValue = ev.target.value;
        setValue(newValue);
        setCleanValue(stripNumbers(newValue));
    }

    return (
        <>
            <div>
                <input value={value} onChange={handleChange}/>
            </div>
            <div>{cleanValue}</div>
        </>
    )
}

// stripNumbers.js
function stripNumbers(text) {
    return text.replace(/\d+/g, "");
}

export default stripNumbers;

Now you can import this function and run tests against it smoothly.

Break components into small pieces

For this example, I am using the list of people. In it, each person has a first name, last name, and date of birth. I want to have the most straightforward examples possible. The final result of the component is in the image below.

Alt Text

For this, we could place everything in one component. And it is not a wrong solution, and if you look at the code, it is easy to read and understand.

function PeopleList({people}) {
    function getPeopleList(people) {
        return people.map(({firstName, lastName, dob}, index) => (
            <div key={`person-${index}`}>
                <div>First name: {firstName}</div>
                <div>Last name: {lastName}</div>
                <div>Date of Birth: {dob}</div>
            </div>
        ))
    }

    return (
        <div>
            {getPeopleList(people)}
        </div>
    )
}

So why and what would we want to improve? What can we do to make this component easier to test? Like in the example before, we can exclude the function in a separate service and have it unit tested. But I want to focus on the size of the component. Functions should not have much logic. And it is the same with components. So proposed solution is to exclude person details into a separate component.

function Person({firstName, lastName, dob}) {
    return (
        <>
            <div>First name: {firstName}</div>
            <div>Last name: {lastName}</div>
            <div>Date of Birth: {dob}</div>
        </>
    )

}

function PeopleList({people}) {
    function getPeopleList(people) {
        return people.map((person, index) => (
            <div key={`person-${index}`}>
                <Person {...person} />
            </div>
        ))
    }

    return (
        <div>
            {getPeopleList(people)}
        </div>
    )
}

Now you do have two components to handle. But if you want to test only how one person’s details are displayed. You can do that. You don’t need to render the whole list, only to test one instance. Smaller components are more comfortable to reuse and to test.

Wrap Up

In this, there is also an element of common sense. I wanted to illustrate two actions you can do to make your testing easier. But they will not always make sense. To decide when to exclude or split something, you can ask yourself if you want to test it independently. Or are you having difficulty testing it because of stuff not related to your component or function? If you are spending time mocking different props or other services you are not using in the area you are mocking, split it. Having many files can sound scary, but you shouldn’t have that problem with proper structure and right naming. And today, most IDEs have excellent search options. Do you have any suggestions or guidelines you are using for testing? Write them in the comment.


For more, you can follow me on Twitter, LinkedIn, GitHub, or Instagram.