When creating a component, an important distinction to think about is whether the component should be a container or presentational component. Here’s how I think about presentational components: they receive state as input and emit HTML as output. Additionally, they communicate actions that have occurred within themselves, e.g. calling an “onClick” callback when they are clicked. To me, this sounds strikingly similar to the notion of a pure function: f(input) = output. Using this model, let’s talk about how to test presentational components.
Testing Pure Functions
The main benefit of pure functions is that they are very simple to reason about — there are a finite number of code paths within them, and the only way to change which path is taken is by calling the function with different inputs. This simplicity extends to testing as well, allowing us to write a test context for each path the code will take and to then feel confident that our tests are sufficient. If we truly believe that presentational components are analogous to pure functions, then this should inform our testing of them.
A Life Counter Application
As an example, let’s build a simple application for keeping track of a player’s life total during a game. This application will display the current life total of the player, as well as provide buttons to increment and decrement that life total. If the life total becomes 0, the message “You are dead” will appear in place of the buttons — there is no coming back from the dead in this game, so the game is over. If the life total becomes 10, the message “You win” will appear in place of the buttons — the game is over once a player reaches a life total of 10.
Visually, it could look something like this:
With the desired behavior in mind, let’s move onto testing.
A Skeleton of the State Space
Before we write any application code, let’s create the skeleton of tests that will assert the behavior of the life total changing buttons:
I find it helpful to write out such a skeleton of behavior before beginning to implement failing tests, and way before writing application code. It helps me envision the component from a bird’s eye view. It also allows me to come to understand the component’s state space — a term I’ve been fascinated with ever since I watched the debate between Jim Coplien and Uncle Bob about TDD. I think of the state space as the universe of possible states a component can be in, and these are most assuredly always finite. This reduces testing to simply thinking about each discrete state that a component can be in at a given time, and testing that state with a corresponding test context. If every single state a component can be in has a corresponding assertion, I feel much safer that that component is doing its job 100% of the time.
A noteworthy difference between presentational components and pure functions is that components can call passed in actions, somewhat akin to side effects. These actions are how components influence state up the component tree, and are a part of their contract. For that reason, they must be included when thinking about the component’s state space, and we should also assert that they are called at the appropriate time.
One tool I use to make this skeleton more evident is a spike — a test-less exploration of a particular implementation. It’s perfectly acceptable to try out multiple implementations without writing any tests in the name of research. In fact, it’s frequently strictly beneficial because sometimes an elegant solution doesn’t present itself on the first attempt.
Here, I have spiked and determined the best way to represent this behavior is with a presentational component that takes in the life total as state, and calls passed-in callbacks when the increment and decrement buttons are clicked. This will allow a parent component to keep track of the actual life total, which is a nice responsibility boundary. Starting to fill in a concrete implementation, here is what the assertions have grown into (I’ve committed to AngularJS for these, though note how almost none of what we’ve talked about so far requires any concrete framework):
I prefer using contexts, even if it’s just an alias for describe (depending on your test framework), and I prefer contexts to override only exactly what they need to set up a particular state context. This informs the test structure, i.e. the extracted setup function.
From here the component basically writes itself, and here is a reasonable implementation:
My views on testing stem from one ultimate goal: to ship software confidently. In my experience, the only way to attain that is to have confidence in testing. Understanding the full state space of components and subsequently testing all states within that space takes away the fear of unpredictable behavior. Luckily, these states can often be reduced to a few buckets with clear boundaries — in this life counter example, the component takes 3 distinct states: When the life total is 0, when it is 10, and when it is in between 1 and 9. Even though 1–9 are distinct numbers, they influence the state of the component in the same way. Finding these buckets is where the art in testing lies.
The full example is available on our GitHub, including a parent component that shows how this presentational component should be used. I encourage you to clone the repo, add behavior, and think about what test contexts are necessary to fully test that behavior. Best of luck on the path to confidently shipping software 🙂
We’re always on the lookout for talented people who want to be a part of what we’re building. You can find out more about us, and see all our open roles, on our careers page.