Behavioral tests with React / Redux

A mature code base is what every developer wants. We write tests, we spend hours thinking about a good design, we look for best practices and we apply them in our projects. At the heart of agile development, we want to write unit tests and to have well tested software.

The concept of behavioral tests isn’t new, I like do define it as “test your application as your user uses it”. It was first introduced to me by Michael Lawrie and Perla Villarreal. This article is about my experience with this subject and how it changed my mind on writing React tests.

When I started studying React, the first thing that I did was create this application. A simple consumer of the pokeapi where the user is able to see a list of Pokémon and can add some of them into an adoption cart.

As we need a List of Pokémon to add a Pokémon to the Adoption Cart it seems more reasonable to start creating our Pokemon List. Considering that we will use TDD for everything, let’s write our first test:

This simple test reveals some small tricks that are really valuable:

  • Use real Redux store
    There are some approaches to test redux connected components. When I first started with React/Redux, I mocked the store by using redux-mock-store. This library is great to validate presentational components. However, it demonstrated hard to refactor; when you change your application state some tests will inevitably fail due to the mocked store having another state.
  • If you need to configure your application state dispatch actions
    If you agree to not mock the application store, you can set up state simply by dispatching actions like it was done on line 21.
  • Create test helpers to show your intention
    Take a look on line 31. You could just find the element inline, but it seems hard to understand. Take a look on the same statement inline:

We can aim for a minimum implementation in order to see our tests go green:

As you can see the list of Pokémon is duplicated, it exists in both the component and the test, but our test is green. The next step is to connect this component with redux for data duplication removal.

You can see the entire commit here and you can see how I removed the code duplication here.

Test your application as your user uses it!

When a button is clicked your user expects that something would happen. But, what is something? Would your user know that an action was called? Should the user know that the state has changed? So then, why should we validate this kind of thing?

Let’s assume that when the SpeciesList is opened a request should be made to the pokeapi to fetch the Pokémon list. We can stub the request instead of dispatching an action.

We are no longer dispatching actions. When you stub a request instead of calling the store action, the test describes exactly what the component does, not in terms of implementation, but in terms of behavior. That’s why there is no action assertion nor state assertion.

When we are working with asynchronous code, as a request is, even when we stub a request it is still a Promise and the .then or .catch methods will not be executed just by mounting the component. If you need to execute any asynchronous code, even that of a Promise that is resolved immediately, you’ll need to wait until all of it is executed.

There are some ways to help with this kind of issue; all of them are pretty similar. You can see at line 45 the ensureRender (look at implementation here). There is a good explanation about setImmediate on stackoverflow.

The reason why the ensureRender receives a wrapper is because enzyme 3 is immutable so you need to call the wrapper.update() manually.

Good Parts of Behavioral tests

  • You are free to refactor
    As you don’t do action assertion nor state assertion, you are free to refactor. If the refactor does not change the component behavior, no test should break.
  • You hardly ever stub or mock
    Unless you are working with requests (I think that you don’t want to call your API in your test), you will not need to stub or mock things. You will use your real action and your real store that uses your real reducer. So there is nothing to mock or stub, you will just use your infrastructure.
  • You don’t need to test your reducer
    My experience is that almost all reducer tests are like Getter and Setter tests. There is no value in this kind of test. In my latest project, we just removed our reducer tests and we figured out that the test coverage didn’t change.

Hard Parts of Behavioral tests

  • A lot of code is needed to get a test green
    As a unit test lover I really like to create small tests where I can see them green as fast as possible, sometimes I feel that a large amount of code is needed to see the test pass. You need to create actions, reducers, etc. It could be painful at the beginning but after few tests you will be able to adjust the test granularity.
  • Sometimes it is hard to detect defects
    I told you that in my latest project we just threw away the reducer tests, this was nice and helped us a lot to refactor. But when a test fails, it needs some investigation to understand where the defect lives.

Integration tests

In the next article I’ll talk a little about integration tests which are the main reason why it is possible to test react+redux without action assertion and state assertion.

Full project

Lead Software Developer at @thoughtworksbr