October 10, 2017

Unit tests 101: What, why and when?

Writing unit tests is an essential skill for any developer that has tremendous benefits to code quality: if done right. This article explains what they are, why we need them and how to overcome challenges that come with them.

Note: I use ES6 throughout this article; check out my in-depth article on ES6 that includes starting kits in Grunt, Gulp and Webpack flavours.

Working in a tech environment, we often hear the following statements:

"it worked yesterday…"

"that’s weird"

"I’ve never seen that before"

"It's not doing that on my computer"

More often than not, these are signs of low confidence in the code we write. Even most senior devs have moments they stare at the screen for hours wondering why their code isn't behaving as they intended it to.

What makes developers less confident in the code they write and how can we make it more predictable?

When features are added to the backlog, they outline the outcome, i. e. the successful result of what the feature is supposed to deliver. Developers write code to satisfy that requirement, and along the way, layers of complexity are added. Library dependencies, databases, integration with other products all introduce new code paths. We may be able to account for a few of these, but as the code grows, even the most gifted simply can't account for all of them.

So what are Code Paths?

Code paths

As the interpreter travels through lines of code it encounters loops, conditions, and expressions all of which create multiple paths for it to traverse. Imagine walking down a path and reaching a fork. You have the option of going left or right; let's say that going left will yield another fork. You now have the possibility of either going right, left and left or left and right. The interpreter travels along your code in a similar way. Let's look at a technical example:

getProducts()
    .then(success)
    .catch(fail);

There's two code paths:

  1. getProducts() call succeeds
    2.getProducts() call fails

Let's add an if statement to check for the number of products:

getProducts()
    .then((products)=> {
        if (products.length === 0) {
            displayNewProducts();
        } else {
           display(products);
        }
    })
    .catch(fail);

There's now an extra path:

  1. getProducts succeeds and there are more than zero products
  2. getProducts succeeds AND there are zero products
  3. getProducts fails

In real-world scenarios, a module is full of such conditionals; add in dependencies from other modules and third-party libraries and a feature that looked trivial at first now has dozens of code paths we can't reliably account for, which is exactly why we need unit tests.

"... a feature that looked trivial at first now has dozens of code paths we can't reliably account for, which is exactly why we need unit tests"


What are front-end unit tests?

Unit tests travel along the code paths of a module or function making sure they yeild expected results.
Effective unit testing involves testing as many code paths as possible in a given function, class or method. It's likely that your customer will hit a given path at least once, so having the unit tests hit it first and ensure there's no unexpected outcome goes a long way.

Front-end unit tests are:

  • a way to test that a given functionality of a module produces the expected results
  • a way to not depend on back-end teams when working on features that rely on APIs
  • a way to prevent blow ups by testing failure cases
  • a way to not break existing functionality in future updates
  • a way to (help) model the back-end APIs

Front-end unit tests aren’t:

  • a way to test your front-end talking to your backend
  • a way to test that module A is talking to module B
  • a way to test DOM updates if using a MV* library

Challenges with front-end unit testing:

Front-end unit testing is more complex than back-end testing especially in the era of client heavy Web Applications. These applications rely on many third-party dependencies, include a ton of asynchronous functionality, and may yield different results depending on the browser.

There's a number of challenges I've come across that make unit tests a

Here's a few challenges developers are often faced with that are also a deterrent from testing:

1. Writing tests doubles development time

This is true - in a way. Recently, I had to go back and retro-fit existing code with tests, and it took almost as long as writing the feature code itself. While adding tests I found areas that could be optimized which resulted in mini rewrites making the process even longer. One thing to note is that writing tests revealed less complex ways to implement the same functionality. Tests break down initial requirements into smaller pieces; they add failure scenarios, and provide a "bigger picture" perspective we often lack when diving right in. The problem is that testing after the fact isn't only slower, it's also less effective.

"Writing tests revealed less complex ways to implement the same functionality."

This is why I stand by Test Driven Development - writing tests before writing any code. The tests will fail, and code is added to satisfy them until they pass. Investing in tests before writing any code makes the development process more efficient, and although it adds "more work", its worth it in the long run. There's many resources on Test Driven Development.

2. Modules are difficult to isolate from dependencies

Front-end modules have many dependencies. These include imports, private methods on the class as well as global objects stored on window. The first rule of unit testing is to isolate the functionality that is being tested from the outside world. In order for unit tests to be consistent, they should be getting predictable responses and results from first and third-party libraries alike. This is where developers need to prepare and setup tests using mocks, stubs, fakes and dummy data.

"The first rule of unit testing is to isolate the functionality that is being tested from the outside world."

Isolating your ES6 code using modern tools is covered in-depth in part two of this series: Unit Tests and front-end development: Isolating Your Code

3. Asynchronous functions, callbacks, promises?

Effective unit testing relies on consistent test results. Asynchronous functions and promises can make testing less predictable and may need a little extra work to set up tests for. Isolating your asynchronous code, using tricks such as setTimeout and relying on testing frameworks tools such as Jasmine's done() helps make this more predictable.

4. DOM tests are flaky

If you're still messing with the DOM, even though it's 2016 and both React and Angular will do that for you (or Elm, Vue, and other underdogs),