How to properly test React Components

Do your tests fail even with a little change of code? Do you mock heavily? Are you testing function calls and component state all the time? Well, you probably doing it wrong.

I have read a lot of articles about how to test React components and most of the time I felt like there should be a better way. In this article, I would like to address some common issues with help of Jest and Enzyme.

So let’s begin with the most important advice.

Test behavior, not implementation details!

Look at the example below.

At first, it seems ok. I have an email input and I want to test “When I change the email input the new value will appear in my state”.

The problem is that you are testing implementation details, specifically the internal component state. And when you couple your tests with implementation details, your tests become fragile.

Look at the property “email” in the state. Whenever you rename that property or refactor your component, your test will fail!

If you are asking “But how the hell do I know whether this input works if I don’t test that” let me show you that you actually don’t need this test.


Creating a feature

Let’s imagine we need a shiny new feature which is a simple subscription form where a user can write his email and subscribe to the news from our product.

Because I pretend to be a professional I am doing TDD so let me write our first failing test.

When a valid email was filled and form was submitted, it should subscribe to the news with correct email

If we fill the email input and click on the submit button, we expect that the subscribe function is called with the email from the input.

We are testing behavior! Not the implementation.

We run the test and it failed because we don’t have any implementation yet. So, let’s write enough production code to pass the test.

In order to make it pass we created the state object with “email” property and implemented the onChange handler on the email input. After that, we created the onSubmit handler on the submit button and called the subscribe function with the email from the state.

Let’s run the test again. It passed.

Our high-level use case forced us to write that code! These use cases are the component’s contract which you should test. We would not test the implementation details like the internal state of the component and how it changes because it is volatile.

Conclusion. Avoid testing component state and don’t dive to DOM in order to find some element you need to test (ex: div > foo > input) because whenever you change your DOM, your tests will fail. We want to depend on stable things, as in life so in software development.


What is a UNIT TEST?

This is another important topic because a lot of people are a little bit confused about what unit really is and this misunderstanding has a huge impact on your tests.

Let’s try to figure it out by extending our previous example.

After some time our component got bigger and we encapsulated some logic into different components. We created a component for email input, submit button and for marketing content which contains some text and logo.

My question is how should we change the tests of Subscription component?

The answer is simple, we will not change anything! 

In fact, our tests are still passing. We were moving, creating and changing things and our tests are still working properly because we did not couple our tests with implementation details!

You might be asking yourself.

Is this an integration test? The answer is no! Let’s bring some definitions from Robert C. Martin.

Unit test. A test written by a programmer for the purpose of ensuring that the production code does what the programmer expects it to do.

Integration Test. A test written by architects and/or technical leads for the purpose of ensuring that a sub-assembly of system components operates correctly. These are plumbing tests. They are not business rule tests. Their purpose is to confirm that the sub-assembly has been integrated and configured correctly.

Micro-test. A unit test written at a very small scope. The purpose is to test a single function, or small grouping of functions.

Functional Test: A unit test written at a larger scope, with appropriate mocks for slow components.

Do you see a word class in the definition of a unit test?

We could imagine a unit as a module. We want to test if we give the module input a, it should return output b. The module is a black box and we are testing only his public API.

A lot of people gets the concept of unit testing as a testing a class (component) in isolation. In order to test this class in isolation, you have to set strict boundaries around that class and mock all of its collaborators. But when you mock all of the collaborators, every time they change your tests have to change as well.


Look again on our previous test.

We can consider our test as a functional test. This is not an integration test because:

  1. we are testing business rules,
  2. we mocked our slow components, more specifically the subscribe call because the production implementation of this function would call probably some HTTP endpoint,
  3. the test is at a larger scope because we are testing whole Subscription component including children presentational components in the hierarchy.

This gave us power because we can even change libraries such as material design, bootstrap or whatever and our tests will be still passing because they do not depend on their specific API.


Bad example of Subscription component’s tests

This is how the Subscription example would look like if you over specified your tests.

Don’t do this!

We coupled implementation details like MarketingContent, EmailInput, and SubmitForm component. What if we change prop “onChange” to “handleChange”? What if we change the names of these components? What if we remove MarketingContent and move that logic to parent component?

Our tests will fail!

But that is not the only problem. Now we have to test all these components as well! And I ensure you that one day we will forget some test and all of these tests become useless because, at the end of the day, our code does not work because we forget to call some method in some child component in the hierarchy.

Summary

Testing is hard, don’t make it harder. Try to not over specify your tests and try to depend on stable things. Yeah, and it is much more fun when you are doing TDD.

Some useful links.

Test definitions from Robert C. Martin — https://blog.cleancoder.com/uncle-bob/2017/05/05/TestDefinitions.html

TDD: Where Did It All Go Wrong — https://www.youtube.com/watch?v=EZ05e7EMOLM