Skip to content

Instantly share code, notes, and snippets.

@ericelliott
Last active November 18, 2017 17:39
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ericelliott/6f9ee798efd00b1b5fda to your computer and use it in GitHub Desktop.
Save ericelliott/6f9ee798efd00b1b5fda to your computer and use it in GitHub Desktop.
software testing

Testing JavaScript Apps

Most apps should eventually have two or three sets of test suites: Unit tests, integration tests, and functional tests.

A note about examples:

We'll be using ES6 syntax just for reading simplicity. () => means function, let is block-scoped var (used here just to remind the reader that we're talking about ES6), and (...args) creates an args array containing all the arguments that follow the ....

Unit Tests

Unit tests validate individual modules by testing the module's API surface. The best use case for a unit test is when a function maps from some inputs to some output in a predictable way. i.e.:

(a[, b, c...]) => d

Say you have a function that generates a sum of all inputs:

let sum = (...args) => {
  let total = 0;
  while (args.length) {
    total += args.shift();
  }
  return total;
};

You might write a corresponding test like this:

test('sum', (assert) => {
  assert.equal(sum(1, 2, 3), 6,
    'should return the sum of all inputs');

  assert.done();
});

In most applicaitons, there will be a handful of functions that are not pure (see the section on stateless functions from Chapter 2 of "Programming JavaScript Applications"). For example, you may rely on a random number generator, a timestamp, or some other unpredictable input source. In those cases, assertions should test that outputs fall into some expected range:

let stringTime = () => {
  return new Date().toString();
};

You can't assert against a specific value, so you should try to get a close approximation instead:

test('stringTime', (assert) => {
  let
    t = stringTime(),
    a = t.split(' ');

  assert.equal(typeof a, 'string',
    'should return a string');

  assert.equal(a.length, 7,
    'string should contain required number of elements');

  assert.done();
});

These assertions are written for tape, but most assertion libraries have similar features. Note that your assertion descriptions should be written to specify intended program behavior (the program should do something), rather than describing the test behavior. That makes the assertions easier to read, understand, and debug when they fail, and it helps better document the intended API.

Note that these are pretty minimal assertions. You could go crazy and make sure that each element is what you expect, but you should temper that based on the project's need for certainty and the likelihood that something might go wrong with the function.

If you need more complete coverage, write it. If you don't, factor in the need to keep the test suite maintainable.

For functions which create side-effects, such as writing output to a screen, database, log file, network, etc..., first, ask yourself if the function really needs to create side-effects. If it doesn't, make it stateless. If it does (some data actually does need to be written to a DB once in a while), there are a couple ways you can test.

First, if you can inject the dependency that is going to be written to by passing it into a parameter, that's ideal. Using that method, you could mock or spy on the dependency and run your assertions against that. An added benefit is that dependency injection can make your application more flexible.

If it's not something that can (or should) be easily mocked, perhaps the tests you need to write against that module should be integration tests, rather than unit tests.

If you're mocking and stubbing a lot of dependencies, or if your mocks / stubs start to cause you test maintenance headaches, it might be a sign that you should really be using integration tests.

For these examples, we'll use tape and faucet for our unit test API and output.

Integration Tests

Integration tests are written to ensure that component interactions behave appropriately. Take the example above where you may want to test that a module can properly interact with a datastore. One way to do that is to mock the datastore dependency and use unit tests.

Another approach is to actually connect to a real database and test that the database state gets updated appropriately when the module interacts with it. If you have a set of modules intended to work together and they will be used much outside the app, or if you're developing mission-critical software and you absolutely must ensure that component interactions are tested thoroughly, you may want to write dedicated integration tests.

Integration tests can be written much like unit tests, except that integration tests will test the interactions between multiple modules, rather than simply testing the surface API of a single module.

There is a superset of integration tests that may make a dedicated integration test suite unnecessary (depending on the needs of your particular app)... Functional tests. We're going to skip dedicated integration tests for this simple example code and use functional tests, instead.

Functional Tests

Functional tests are designed to test the functionality of the application as a whole. A functional test starts from user stories like "as a user I want to log in so that I can access my account data". A functional test will typically mock user behavior instead of component behavior. In other words, it will generate UI interactions, and then test UI outputs and ensure that the app responds appropriately.

Since functional tests require the entire system to be operational in order to be trustworthy, functional tests are typically the most complex, and take the longest to run.

It's usually a really good idea to create a thorough enough functional test suite to ensure that all of the "happy path" user behaviors work appropriately. That means login / logout, checkout flows, and all of the essential operations a user may want to perform to derive value from your app.

For functional tests, Selenium Webdriver is the gold standard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment