Test-Driven Development for JavaScript

Written by: Taylor Jones

JavaScript is handsdown the strangest language I’ve ever had to test. Its also one of the most popular ones out there right now. The influx of JavaScript developers tells us that a lot of modern-day web development is starting to focus more and more on the frontend.

This trend is interesting because it represents a shift in development that we’ve never seen before. Our applications are being composed of more and more JavaScript. However, are we effectively testing all of this newfound client-side code?

Sort of.

I believe that we must follow a more robust methodology to test our growing JavaScript applications. Today, I want to explore the idea of practicing Test-Driven Development (TDD) for client-side JavaScript.

What’s So Different About Testing JavaScript?

In web development, we can separate our concerns into two sections: the client-side (stuff that happens in a user’s web browser) and the server-side (things that happen on our application servers).

When it comes to testing, we’ve historically covered our server-side code with unit and functional tests and our client-side with integration tests. We tend to give a lot of thought and investment into server-side tests and simply verify the results with an integration test on the client-side.

However, server-side code is and was never the complete picture of web development. What the user sees on the client-side is often what has the most impact. The most impressive technical features mean nothing if the view layer is messed up!

Integration tests are helpful and effective when we’re only concerned with what shows up on the webpage. It says nothing about what goes on inside the client-side code processing; it only shows the output of such logic.

There was a period of time where integration tests could really serve as proper test coverage for what’s going on in the client-side of our applications. However, with more and more applications having bigger shares of JavaScript, we’ve got to come up with a better way of testing the client-side other than: is x showing up on the page?

To start things off, let’s look at what’s familiar about TDD on the client-side. It turns out a lot of things aren’t that different after all!

JavaScript TDD Isn’t That Different

No matter how experienced in TDD you are, don’t panic! If you’ve done TDD before, applying it to client-side JavaScript isn’t that different on a high level. With TDD, we want to stay true to a cycle of three steps:

  1. Write tests that reflect the expected behavior of your code.

  2. Write code to make those tests pass.

  3. (optional) Refactor and ensure the tests still pass.

Let’s run through a quick example in EmberJS.

A unit TDD example with EmberJS

Ember is really cool because it comes with a lot of great tools for testing right out of the box. If you run something as simple as: ember g model user, you’ll be gifted with two important files: app/models/user.js and tests/unit/user-test.js. If you run ember test, all of your tests related to user.js should pass.

Currently, our files look something like this:

user.js

import DS from 'ember-data';
export default DS.Model.extend({
});

user-test.js

import { moduleForModel, test } from 'ember-qunit';
moduleForModel('user', 'Unit | Model | user', {
  // Specify the other units that are required for this test.
  needs: []
});
test('it exists', function(assert) {
  let model = this.subject();
  assert.ok(!!model);
});

We want to expand our model to do two things: have a name and be able to retrieve a metric called “karma” for a user. Let’s write up some tests to reflect these desires:

user-test.js

import { moduleForModel, test } from 'ember-qunit';
moduleForModel('user', 'Unit | Model | user', {
  // Specify the other units that are required for this test.
});
test('it exists', function(assert) {
  let model = this.subject();
  assert.ok(!!model);
});
test('must contain a name', function(assert) {
  let model = this.subject({ name: 'testing name'});
  assert.equal(model.get('name'), 'testing name');
});
// Note: Karma should be the combo of upvotes and downvotes against a user
test('should be able to retrieve the karma of a user', function(assert) {
  let model = this.subject({ name: 'karma user', upvotes: 10, downvotes: 5});
  assert.equal(model.get('karma'), 5);
});

If we’ve changed nothing to user.js, we’ll find that our Ember test suite will now fail gracefully. Next up, we’ll work to make these tests actually pass.

user.js

import Ember from 'ember';
import DS from 'ember-data';
export default DS.Model.extend({
  name: DS.attr('string'),
  upvotes: DS.attr('number'),
  downvotes: DS.attr('number'),
  karma: Ember.computed( function() {
    return this.get('upvotes') - this.get('downvotes');
  })
});

Now our tests and code are good to go!

With just a few simple steps, we were able to run through the TDD process and develop some simple but stable code! I chose not to refactor on this run through since my changes were pretty simple. However, the more complex our code becomes, the more there becomes a need for refactoring at every step.

Other frameworks and JavaScript code

Not every JavaScript framework or codebase has a built-in test runner like EmberJS (or EmberCLI) has. There still are a lot of excellent tools out there like Mocha and Jasmine. However there’s still a long way to go when it comes to JavaScript test tooling.

While client-side unit testing doesn’t look that different from other kinds of testing we’ve experienced, there are some noticeable differences to how we should approach it.

Javascript TDD Is Different

When implementing TDD on the client-side, we’ll need to divide our testing mindset into client-side and server-side concerns.

Client-side JavaScript cannot query a database. It can make calls for a query or query data presented on the client-side. It cannot, however, directly fetch and insert into a database. This is important because we want to assume that the data being given to the client-side (in our tests) is gospel. We should treat the server-side data being sent as an external dependency on our test.

In many ways, we’re splitting up our applications into two contained testing areas: Server-side tests are concerned with things that go on inside the server; client-side tests are concerned with stuff that happens in the web browser. In order to achieve proper test coverage of these two aspects, the amount of tests we’re hauling along is going to increase. In some cases, we could have all of the following:

  • Client-side integration tests

  • Server-side functional tests

  • Server-side unit tests

  • Client-side unit tests

  • Client-side functional tests

Adding a lot more test weight to your application might not seem like the most attractive option. Yet, its a consequence of having complex processing on the server- and client-sides of your application. There’s a lot more space to be covered, and understanding more of what’s going on is essential!

What Does All This Mean?

If you’re implementing TDD practices for the first time, JavaScript is a great place to start. The basics of TDD aren’t too hard to get down, but it takes some practice to grasp what aspects of the domain you should test.

If you’re starting to apply previous knowledge of TDD to your frontend JavaScript, don’t forget what you know. Embrace your historical knowledge and learn to understand the domain layout of the client-side.

Ultimately, JavaScript test coverage is a hard thing to fully visualize and understand. The way we leverage the language to help us develop apps is ever changing as well. You might be adding a lot more tests to your test suite, but the understanding and coverage you’ll have is worth it!

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.