Menu Navigation Menu

If you work on a lot of react projects, you’ve probably seen some interesting unit test suites. After all, there’s lots of schools of thought around how to properly test react applications, right? Some folks will sinon.spy() everything and play with the require lookup. Others might connect all their components to redux or graphql, and in so doing ensure that they can only test their components in the context of that state management.

With all that in mind, and at great risk of xkcd-ing myself, here’s a couple unobtrusive guidelines to help streamline your experience of testing react-based applications. Before we dig in though, I need you to buy into a couple of baseline assumptions that I’m making.

The first assumption seems pretty obvious on its face: plain functions are easiest to test. You have an input, and you can test whether the function returns the correct output. The second assumption is mathematics, and rather more complicated: functional composition does not warrant testing. This is specifically in reference to the high school algebra sense of the word, where for some function f and some function g, you can compose them into a new function that looks like f ○ g. in code, that means g(f(x)).

Now, that doesn’t completely hold up here because we’re talking about Javascript functions and not y = mx + b. So we need to add a caveat: this holds true for your functions when you can agree on a contract of signatures. This becomes much easier if you’re using flow or another static typing system, but it’s also possible to do the work to validate your parameters and check their types manually at runtime to ensure that contract. Either way, you need a common interface.

With those things in mind, let’s look at some concrete strategies for architecting your react components, starting from a goal of testability.

Favor Functional Components

This is more of a project architecture tip rather than a test infrastructure one. When writing react apps, it’s very important to favor functional components. This seems to flow easiest out of our first assumption above – if plain functions are easiest to test, make more of your components into plain functions. This also helps you because it encourages you to push state upward—since functional components can’t contain state, favoring functional components promotes a relative “hub and spoke” architecture wherein “hub” components for distinct views own a number of functional components which handle presentation.

Test Unconnected Components

As far as test architecture and techniques go: as tempting as it may be to wrap everything in a <Provider> and start fiddling with a mocked version of your redux store, it’s usually a better idea to test the unconnected components. And while you’re at it, test your mapDispatchToProps and mapStateToProps functions, too.

The rest (namely the connect() call), is functional composition (and thus doesn’t warrant testing, per assumption number two above).

Decouple Implementation from Tests

Lastly, I want to talk about what you’re testing.

If you find yourself having to refactor all your tests every time you change your code, you might have a unit test that is tied too tightly to the internal implementation of your function. Your unit tests shouldn’t care how your code got the right answer. They could be shelling out to a ruby script that finds the correct data – from your test’s perspective it should be all the same. This is close to what we in the functional programming space call “referential transparency” – if you can remove a function call, replace it with the correct answer, and effect no meaningful change in the system, your function is referentially transparent.

Test your components like they’re functions (which they probably are). Give them an input, make them give you an output, and compare the output to the correct answer. That’s all you have to do; don’t tie your test to “what does the component under test have in this.state?” Make sure your components do the thing they should, but don’t smother them.

It should be noted that, while these strategies are a great idea in an ideal situation, they are not universally applicable. You alone know the state of your codebase and whether it will benefit from this sort of architecture. If you’re there, great! Go make a new branch and get cracking. If you’re not quite there yet, we can help get you there.

Contact us for a complimentary 30 minute consultation.

get in touch