Speeding Up Your JavaScript Test Suite

Having fast tests is important. Slow running tests slow down development, especially if you’re practicing TDD. If tests are too slow to run, some developers may avoid running them altogether. Slow tests will also slow down CI builds, increasing the length of your feedback loop.

While it takes more development time, doing maintenance on your test suite to ensure it continues to run quickly is an important task that any significant project should prioritize.

I recently went through the practice of speeding up a large JavaScript test suite for the project I am working on. I thought I would share some of the culprits I found that were slowing down my tests, and how to fix them.

Tooling

If you are going hunting for performance problems in your code, it can help to know where to start looking. Getting a report of your slowest tests, or tests that run longer than some threshold, can be a great way to get starting metrics.

Some test runners have this feature built in, for example the Karma JavaScript runner has a reportSlowerThan option which does just this. If it’s not built into your test runner, it can be pretty easy to hack in manually by starting a timer at the start of a test in a before block, and then reading it in an after block.

1. Audit your beforeEach code.

This may seem like a no brainer, but it’s important to remember that any code in a beforeEach block will have linear complexity, where n is the number of tests. If you have some type of global before and after blocks, this can be particularly troublesome. Keep an eye out for slow running computations, loops, and DOM manipulations. While I am specifically talking about speeding up JavaScript tests here, this is a good tip for speeding up any type of test suite.

One of the first problems I found in my test suite was some code in a global beforeEach block that was inserting some static content into the DOM before every single test. Removing that so that it only happened once reduced my test execution time by more than half. It was the single biggest improvement I was able to make.

Don’t assume that your own code couldn’t possibly have something so simple slowing it down. It went unnoticed on our project by a whole team of engineers for well over a year. It took someone actually wondering why the tests were so slow to notice it.

2. Avoid rendering things onto the page.

As evidenced by my previous example, DOM manipulations and insertions can be expensive. Most JavaScript code can be tested without rendering anything onto the actual page, and doing so will make your tests run much faster. It can be tempting to just test JavaScript code by drawing views or components or whatever and interacting with them, but this does not scale well. Make sure you only touch the DOM when you really need to, and that you limit the scope to individual tests that need it. If you need to do it for a large number of tests, consider whether you can just render something once, and then reuse it across tests.

3. Use sinon or another tool to test async things synchronously.

Asynchronous behavior is a reality of JavaScript programming. It can also seriously slow down your test suite if you are having tests wait for timeouts or AJAX calls to complete.

Most javascript test frameworks give you the ability to run tests ansyncronously and wait for async behavior in your code to happen. For example, in jasmine and mocha you can do:

Code


myAsyncFn = function() {
setTimeout(function() {
counter++;
}, 500);
};

Test


it('increments the counter after a wait', function(done) {
counter = 0;
myAsyncFn();
expect(counter).toBe(0);
setTimeout(function() {
expect(counter).toBe(1);
done();
}, 500);
});

Unfortunately, this test is slow. It will sit and do nothing for 500ms while waiting for a timeout to trigger, meaning this whole tests will take more than half a second to complete at the absolute minimum.

Fortunately, there is a better way. For the purposes of testing, we can turn this into synchronous code using sinonJS. Sinon has lots of tools to turn async behavior like timeouts, intervals, and AJAX into controllable synchronous code. We could easily use it to rewrite the above test to be synchronous, and much faster as a result:


it('increments the counter after a wait', function() {
  var clock = sinon.useFakeTimers();
  counter = 0;
  myAsyncFn();
  expect(counter).toBe(0);
  clock.tick(500);
  expect(counter).toBe(1);
  clock.restore();
});

Out test suite had several complicated async tests that were taking several seconds to run when written in the former style, plus a host of simpler ones that were still much slower than they needed to be. Switching them to use sinon timers shaved around 20% of our total test execution time, and also made the tests easier to read and debug.

Tests Need Maintenance Too

Keeping your test suite healthy and fast is an important maintenance task for any project. What do you do to keep your tests running quickly?

Conversation
  • David Tang says:

    awesome post. really helpful! thanks!

  • Comments are closed.