JavaScript and React unit tests basics

Luís Cardoso
Feedzai Techblog
Published in
9 min readMay 30, 2019

--

This is the first part of a two-article series where we will cover all you need to know to get started on writing your first Javascript and React unit tests.

Why do we need unit tests?

Unit tests give us a high degree of confidence that our code behaves like we expect it to.

This is important in two moments: when first writing a piece of code and when we need to change the code later on.

What are unit tests?

Unit tests verify that the functions in the project produce the outputs we expect for a given input.

Lets drill down on that a bit.

The unit tests should not be on a separate project. This is important because unit tests are closely related to the source code, and having them on a separate project would make it much harder to implement and maintain the tests.

The purpose of unit tests is to validate individual functions. The fact that they only test individual functions increases test speed so we can easily run the whole test suite locally. That also makes them easier to write, maintain, and debug. This is not to say that other types of tests, like integration tests, are not important. We just don’t want to mix unit testing with other types of tests. If we do, we risk losing the advantages of unit tests.

Unit tests fulfil their purpose by performing assertions. Assertions are equality checks. We check that the output of a function is what we expect given the passed input. In modern libraries, assertions are a bit fancier. We’ll expand on this later.

What unit tests are not

There are three main things that unit tests must avoid at all costs:

  1. Tests should not depend on how a function is implemented. They should only depend on the outputs of the function for a given input.
  2. Unit tests must not, in any way, depend on having a running application. They must run offline.
  3. They must not check integration between components.

Coverage

Coverage is a very important concept in the world of unit tests. Coverage is usually a set of metrics that indicate how complete the unit tests are.

Coverage works by indicating which pieces of code were exercised by the unit tests. That is, if that code ran while the tests were executing.

You do not want tests that perform an action, thus having coverage, but actually do not assert anything. These are tests that don’t check if the result of the action is correct.

There are several types of coverage. The most important are:

  • Line coverage — indicates if the line of code was exercised
  • Branch coverage — indicates if the branch of code (i.e., an if/else path) was exercised
  • Function coverage — indicates if the function was exercised

100% coverage does not mean your code is bug free, but it can be an indicator of quality (if you didn’t cheat when writing the tests).

The minimum threshold for acceptable coverage is generally considered to be 80% across those metrics, but there is no reason not to aim for 100% if you can.

Methodologies

Methodologies are frameworks for software development that include tests as part of the development process.

The only formal unit testing methodology that is commonly used, at least in the frontend world, is Test Driven Development or TDD for short. The idea behind TDD is to write the tests first and do the implementation after.

Whether this is practical or not depends on your way of working and the project. Personally, I think this methodology makes a lot of sense for libraries. In those cases, it is easier to have a clear notion of what we want the method signatures, the inputs, and the outputs to be.

In my experience, implementing a user interface is more of an iterative process. It’s often hard to define the exact API of the components before implementing them.

In practice, what I recommend is:

  • Use TDD if you are implementing an isolated library-like module. If you’re not implementing an isolated library-like module, it’s best to implement first and then conduct the tests. The important thing is to make sure you don’t implement too much without writing any tests. Ideally you should implement incrementally, and at the end of each step, do the unit tests.
  • Write unit tests daily. Do not code for days, or worse weeks, and leave unit tests to the end. By that time, you will have lost most of the context. Writing unit tests is inherently dull; it’s a daunting task to write them for a week’s worth of code.

Best Practices

Key takeaway

It’s crucial to have tests from the start. If you don’t have unit tests from the start, your test coverage will be, at best, mediocre, and it may take years to reach good levels.

On a side note, a linter is another thing that is good to have from the start. It is much more difficult to add a linter later on. (Use eslint, it’s great.)

Continuous integration

When working in a project with several people it is useful to have your tests run in continuous integration. What this means is that you have a server where your can run your test suite. This allows you to rerun the unit tests automatically, send alerts if someone broke something, and to automatically run the unit tests on new pull requests. You can even calculate the coverage after the diff.

Having the linter run in continuous integration is also useful for the same reasons.

Do not test the framework

Example 1. An example of testing the framework

Testing the framework instead of testing the code is a very common error in unit tests. In the example above, we are testing the html method of jQuery. This is unnecessary as jQuery already has a complete suite.

If for some reason you need to test that the behavior of a library is consistent (for example, to make sure the behavior of a method did not change between different versions of the library), you should do that in a specific place where you only test the library.

Try to avoid asynchronous calls

Asynchronous calls can introduce lots of complexity, especially in more complex code. You should try to the best of your ability to write your tests synchronously.

For example, in React, setState is asynchronous. However, if you test your component using Enzyme, setState will actually be called synchronously. That helps a lot when writing tests.

On the other hand, you can usually test AJAX requests asynchronously if you have the request isolated on a method. This is because in these cases the code is usually simpler, and we don’t have a long chain of asynchronous calls.

If you find yourself needing to test calls that somehow involve time (e.g., setTimeout or setInterval), I strongly recommend not using the timer mocks that some libraries provide. I’ve found that tests written with these mocks are hard to understand and maintain. In my experience it is much better to isolate the time manipulation logic in one or more methods and then stub those out.

Test one thing at a time

Tests are much easier to maintain if each one tests just a specific behavior. If you want that behavior to change or if it doesn’t make sense anymore, it is easy to update the test. If the test starts to fail, then you know which behavior is broken. If a test asserts multiple behaviors, you need to look at the test to know which behavior was broken.

Example 2. In this example we are testing two different behaviors in the same test.
Example 3. In this example, we separate the two different behaviors into separate tests, and we move the common logic to a setup phase.

Describe how, what and when

The test description is a string that describes the behavior being tested. To make the behavior clear, I suggest using a how, what, and when structure. The “how” is a verb that indicates the action being exercitated (e.g., “returns” or “renders”). The “what” is the object of the action (e.g., “null”, “form”). The “when” is the condition on which the action happens (e.g., “if the user was not loaded”).

Some examples:

  • Do: “returns null if the user was not loaded”
  • Do: “renders the button if the user is logged in”
  • Do: “calculates the nodes to remove”
  • Don’t: “work as expected”
  • Don’t: “renders”
  • Don’t: “completes successfully”

Clearly distinct phases

Any kind of test will have at least two clear phases: an action and an assertion. Depending on the test, there might also be a setup phase where any necessary context is prepared. This setup might require a cleanup so that other tests can run correctly. In rare cases, this is inevitable. Nevertheless, the need for the cleanup phase should be avoided, as explained in the next section.

Example 4. In this example we can clearly see the three different phases (in practice we don’t need to have those comments). A counter example of this would be Example 2.

Avoid the need to clean up

Sometimes there is a need to set-up something before the test starts. For example replace an API method that would otherwise try to make a backend call. Ideally, you should replace that API in a way that affects only that test. Put another way, when the test finishes running you don’t need to rollback any changes made to state outside of the tests.

As an example, imagine that you have a User class with a loadPosts method that you need to replace. If you replace the method in prototype of the User class (the prototype is the class definition), then at the end of the test you need to roll that change back. If instead you create a User instances just for that test and override the method at the instance level, then you don’t need to worry about cleaning up because that instance will be discarded when the test finishes.

Example 5. In this example we need to replace the loadPosts implementation. We do so by replacing it on the User class prototype. Because the User class is shared between test,s we need to undo the change after the test.
Example 6. In this example we override the loadPosts method by overriding the implementation at the instance level. Because the instance is discarded after the test finishes, there is no need to clean up after the test.

Avoid changing global state

Related to the last point: write tests in such a way that you never need to mutate global objects. All state must be local to each test; there should not be any state passing from test to test.

Have a standard structure

It’s important to have a standard test structure. This allows you to easily know where to find tests for a specific file and method and where to add new tests.

To organize the test files, I recommend having a test file for each source file (except logicless files like enuns or configs) and locate your tests either:

  • In a separate folder that mirrors the folder structure of the source files. In this case, I recommend that both files have the same name;
  • or have the test fields side by side with the source file.

To organize the tests inside the test file, I recommend structuring the test file by method name. Have the methods in the same order as in the source file.

As an example, if I had a class like this:

The test structure would be like this:

These may seem like little details, but they are very important, particularly in larger projects, to make sure we can move around the codebase fast.

Current concepts and tools

Spies and stubs

Two common necessities when unit testing is to have the ability to know if a function was called and to replace a function’s implementation by a dummy one.

For these two jobs we have spies and stubs.

Spies are callbacks that keep track of how they have been called. Typically the number of times called or parameters passed to it are stored by the spy. This allows you to perform assertions based on the values the spy function keeps track of. It also allows you to know if the function that was supposed to be called was indeed called.

Example 4 is a clear example of a spy, which in this case is used to know if the AJAX method was called or not.

A stub is when you replace a method of a module or class with a mock implementation. A stub will also work as a spy in the sense that you can know how it was called.

Example 6 is a clear example of a stub, which in this case is used to replace the implementation of the loadPosts method so we can focus the test just on the searchPosts method.

Some unit test libraries provide you with spies and stubs. There are standalone libraries, like sinon.js, that implement these functionalities as well.

As a final note, in some libraries (like Jest, which we will see next) there is no distinction between the spies and stubs — they exist together as a single API.

Jest

Jest is a test runner developed at Facebook. It is primarily targeted at React developers, but it can be used to implement any kind of Javascript tests.

Jest has several interesting features for developers:

  • Tests run in parallel to maximize performance
  • Sandbox files and implements automatic global state resets for every test
  • Easily creates coverage reports out of the box
  • In most cases, works out of the box without any configuration (looks for tests in a __tests__ folder and looks for files with .spec.js or .test.js extension)
  • Has mocking capabilities out of the box
  • Has snapshots, a feature to keep track of complex objects in tests

Enzyme

Enzyme is an alternative render target for unit tests. Instead of rendering your React components to be able to test them, Enzyme allows you to render components to memory (and never touch the DOM).

Since Enzyme doesn’t need the browser to render the React components, it’s faster. It also allows for some interesting features such as shallow rendering or synchronous setState to ease testing.

Without going into much detail, shallow rendering allows you to test only the component and mock out the complex children of your component.

Recently react-testing-library appeared as an alternative focused in good practices. I haven’t yet used it so I can’t recommend it.

Wrapping up

We have covered all the theory that you need to be able to write good and maintainable unit tests.

In the second article of this series, we will put these concepts into practice and implement our first unit tests!

--

--