Unit Test Frontend Components

Understanding what should we test

José Silva
The Startup
6 min readOct 16, 2019

--

Photo by Mark Oates from Burst

Testing our code is (or should be) an integral part of our daily work. It brings us more confidence, more quality, and better documentation.

With a good percentage of coverage, everyone has less fear to change code, and consequently, it leads to more happiness.

Photo by Pixabay on Pexels

The first question we ask ourselves when we start writing unit tests is, “What should I test?”. Answering wrongly to this question can result in too much or too few tests. We only need the right amount!

Let’s start with the basics.

Unit Test

Unit testing consists in isolating part of our code, like a function, call it with some inputs and verify if it returns the expected result.

What should we test in this function?

  • When we call the function “sum” with two numbers (inputs), we are expecting to receive a specific result (output).

Unit Test Frontend Components

For frontend components, the concept is the same, but instead of functions, our unit is the component.

An excellent explanation can be found on Vue Test Utils documentation:

For UI components, we don’t recommend aiming for complete line-based coverage, because it leads to too much focus on the internal implementation details of the components and could result in brittle tests.

Instead, we recommend writing tests that assert your component’s public interface, and treat its internals as a black box.

For example, for the Counter component which increments a display counter by 1 each time a button is clicked, its test case would simulate the click and assert that the rendered output has increased by 1. The test doesn't care about how the Counter increments the value, it only cares about the input and the output.

The benefit of this approach is that as long as your component’s public interface remains the same, your tests will pass no matter how the component’s internal implementation changes over time.

It is also important to highlight that:

  • Each test function should test a single concept. Test descriptions must be short, precise, and need to reflect exactly what we are testing. We must not have to read its implementation to understand it;
  • We should mock component dependencies when possible. Our focus is the component, its dependencies (child components, store, router, API calls/responses, and even third-party libraries) must have their own tests. Mocking the dependencies will make our tests run faster and allow us to avoid retesting them. It will also give us more control over the component inputs;
  • We should trust the framework and not test it;
  • It is crucial to test error scenarios. A lot can go wrong in our applications, but some things, like API requests returning errors, we can predict. We must test that our app reacts as expected in those cases;
  • Use test coverage as a helper to determine if we forgot to test something but not as a source of truth that everything was tested. You can have 100% coverage without testing all you need!

Inputs & Outputs

Inputs are everything that can result in a side effect (output) on a component.

Some of the more frequent inputs and outputs of a component

The most simple inputs to identify are props and user interactions. Almost every component has at least one of these inputs.

As we increase the complexity of a component, other kinds of inputs start appearing. Data resultant from a store (Redux, Vuex), API responses or router information are very common ones.

Most components end up with other components inside, and when a child component emits an event, it is considered input on the parent component, if it reacts to it.

All these inputs cause some side effect on our component. It can be the render of a label (DOM), an event being sent, a store or an API being called, data being sent thought props to a child component or even our URL being modified (Route).

Documentation

Once we have a good percentage of coverage, tests are the most detailed documentation we can have. However, it is easy to end up with test files that are hard to read or to find anything.

To avoid that mess, a good structure is necessary:

Example inspired on Matt O’Connell’s excellent presentation

Following a strategy like this, test descriptions become shorter, and it is easier to find a specific test or to check what still needs to be tested.

In the example, I use describe as an aggregator of type of inputs and the it always starts specifying the input being tested.

Practical example

Now that we introduced what should we test and how should we structure our test files, let’s try to apply it to a concrete example.

In this example, we have two components, UsersList and UsersListRow. To test them, we do the question: What does each component really do? Let’s isolate each, visually, to quickly answer.

UsersListRow

UsersListRow component

What does it do?

  • Receives and displays a prop “name”;
  • Emits a ‘view’ event, with the prop “id” as a payload, when we click on it;
  • Emits an ‘edit’ event, with the prop “id” as a payload, when we click on the edit icon;
  • Emits a ‘delete’ event, with the prop “id” as a payload, when we click on the delete icon;
UsersListRow Inputs & Outputs Diagram

UsersList

In the following image, the content of the UsersListRow is empty because it does not matter. We need to focus on what the UsersList component does and to that, we only need to know the public API of the UsersListRow component.

UsersList component

What does it do?

  • Receives a list from the store and renders the respective number of UsersListRow.
  • Receives the total number of items from the store and renders a “load more” button when the number of shown items is lower than that;
  • Calls the store to fetch more items when we click on the “load more” button;
  • Opens a detail page when a UsersListRow emits a “view” event;
  • Opens an edit page when a UsersListRow emits an “edit” event;
  • Calls the store to remove an item when a UsersListRow emits a “delete” event;
UsersList Inputs & Outputs Diagram

Once we specify what each component does, we can efficiently structure our test files.

Not going outside the component scope, allow us to refactor the internals of UsersListRow without having consequences on the UsersList tests.

Note

Changing UsersListRow to emit a ‘remove’ instead of a ‘delete’ event would break the unit tests of the UsersListRow but not the UsersList tests. Integration/Functional tests are the ones responsible for catching this problem.

Conclusion

With the right mindset, writing unit tests can be enjoyable and lead to higher quality and more confidence in our work.

A good organization of our tests can result in better documentation and can help us to understand what is already tested and to navigate through them quickly.

To answer the question “What should I test?” we must be able to answer the question “What does the component do?”.

Thanks for reading. You can learn more about me on Medium, Github and Linkedin.

--

--

José Silva
The Startup

Lead Frontend Developer @ ActivePrime, Vue.js enthusiastic.