Quick JavaScript testing tip: How to structure your tests?

Tags:


Something that’s not talked very often when it comes to writing unit tests is how should you structure your test code?

I’m not talking about “well, just put your code into a file in test/SomeTest.js”, but rather, how should the test code itself within your test case be structured?

While it may feel inconsequential – afterall, it’s just a test, and it’s probably just a few lines of code – it’s actually very important to take care when writing your tests. The most annoying piece of code to fix or work with is the code you can’t make any sense out of. Tests are code. As such, it’s important to treat your test code just like any other code, and make sure it’s easy to understand!

Let’s take a look at how we can structure our test code to make sure our tests are easy to understand and follow.

Using describe and it

Let’s first quickly go over some ways you can use describe and it to create logical groupings of tests.

An easy pattern to start with is using a describe on the top level telling the name of the module or file. Each test case inside it can then describe behaviors of that module.

describe('image-resizer', function() {
 
  it('should keep aspect ratio of landscape images if aspect ratio is enabled', function() {
    //test code...
  });
 
  it('should keep aspect ratio of horizontal images if aspect ratio is enabled', function() {
    //test code...
  });
 
  it('should compress image', function() {
    //test code...
  });
 
});

For simple modules, this is often good enough. But when you start having more and more tests, it can be useful to further group things using nested describes. One good way to use nested describes is to group things by sub-feature. If you look at the example above, you’ll notice two of the tests have a fairly long description. Both of them also mention “when aspect ratio is enabled” – this can be a sign we could group them using a nested describe.

describe('image-resizer', function() {
 
  describe('Keep aspect ratio enabled', function() {
    it('should keep aspect ratio of landscape images', function() {
      //test code...
    });
 
    it('should keep aspect ratio of horizontal images', function() {
      //test code...
    });
  });
 
  it('should compress image', function() {
    //test code...
  });
 
});

Another good way of using nested describes is to group by function. Sometimes you’ll have a module with several functions, so grouping by function can make sense.

describe('Math', function() {
  describe('abs', function() {
    //tests for Math.abs
  });
 
  describe('floor', function() {
    //tests for Math.floor
  });
});

As a side note, another use for nested describes is constraining test hooks. If you have some tests which require complex initialization logic, but not all of your tests do, you might want to group those tests into their own describe. You can then move your beforeEach hook into the describe, which means the logic only runs for those tests. This helps make your tests easier to understand – it can be confusing as to whether a certain test needs specific beforeEach logic or not, so by grouping you reduce the chance for confusion.

Arrange-Act-Assert

Now, let’s look at how to structure the actual test code itself. This is where for the longest time I thought “anything goes” – nobody ever said “ok here’s how you should do it”. As a result, my test code ended up being a bit messy and unorganized.

There’s actually a very straightforward technique for organizing test code: Arrange-Act-Assert.

You might already get an idea of what that looks like based on its name. The idea is we first “arrange” our test data and variables, then “act” on it by calling the function we’re testing, and finally “assert” that we got the correct result.

In practice, it means our tests end up looking something like this:

it('should do something', function() {
  //arrange...
  var dummyData = { foo: 'bar' };
  var expected = 'the result we want';
 
  //act...
  var result = functionUnderTest(dummyData);
 
  //assert...
  expect(result).to.equal(expected);
});

First, we set up the data we’ll use in our test. It’s not 100% required to use variables, but especially in cases where the same value is shared or used multiple times it’s useful to do this. In this case, I’ve also stored the expected result value into a variable – which can again be useful so that you can quickly tell what is the expected value.

Next, for the act step we call the function we’re testing. In some cases this could be more than one line of code if necessary.

And finally in the end, we simply check the result was correct. Typically for unit tests, it’s recommended to use just a single assertion – if your test seems to need more than one assertion, you should consider splitting it into two. However, as with most things, this is not a hard rule so use your judgement to decide what works best.

If our tests follow this pattern, anyone can easily look at them and quickly see what’s going on – of course you don’t need to include the comments telling which part is which. Instead, a good practical method is to simply separate the different parts by a newline.

Given-When-Then

There’s also an alternative way of thinking about your test structure. This came up in an email discussion I had with Gerard Meszaros.

The idea with Given-When-Then is similar to Arrange-Act-Assert – it simply replaces the verbs with its own, the structure remains the same.

  • “Given” some test data (same as Arrange)
  • “When” we execute the function being tested (same as Act)
  • “Then” we get some result (same as Assert)

This idea comes from the BDD-world, specifically BDD testing tools like Cucumber where you use human-readable “user stories” to describe your tests.

So what’s the point of Given-When-Then with unit tests then if it is essentially the same as Arrange-Act-Assert?

The idea is similar to the BDD-style test structure we use with Mocha and similar tools. By using more “behavior oriented” language, it helps us focus on the fact that we’re working with behaviors, instead of specific actions in code. If we think too much about imperative code steps, we can easily end up with tests that focus too much on the implementation details. This in turn leads to brittle tests – let’s say we change the implementation of a function slightly. While this might not change the overall “behavior” of the function, if our test is too focused on the implementation detail, then our test breaks very easily.

Conclusion

With smart use of what our tools provide us with – describe and it – and knowing some good patterns, we can make our tests much easier to understand and work with. This means we’ll need to spend less time maintaining and fixing broken tests, which is always a win.

Whether you prefer Act-Arrange-Assert or Given-When-Then, just make sure to not focus too much on implementation detail to avoid issues with brittle tests.