Building Test Fixtures to Clean up Mocha / Jasmine Tests

One of the problems with unit testing complex software is that the tests often become as complicated as the system under test. This eventually leads to bloated test code that is hard to read and maintain, and is usually littered with code duplication as the test base grows.

test-fixture

A test fixture is a device or setup designed to hold the device under test in place and allow it to be tested. In software testing, a test fixture is a fixed state of the software under test used as a baseline for running tests.

In this post I’ll demonstrate how I’ve been cleaning up my unit tests by building “test fixtures” using nested beforeEach() and afterEach() functions in my tests.

About the Code Samples

These code examples are testing a view in a Backbone / Marionette single-page application. You shouldn’t need to know Backbone in order to understand the code though!

The code shown here is using Mocha and Chai (expect syntax) but will be almost identical in Jasmine.

I’ve tried to keep these code samples as close to the actual production code as possible, but some minor changes were made for readability and to protect the IP of my employer.

Cool, so what are we testing?

I have a view in the application that displays a table of users in the system. There are “edit” and “delete” buttons for each row, and a “new user” button above the table.

If the currently logged-in user has “edit” permissions, then these “new”, “edit” and “delete” buttons are visible. Otherwise they are not rendered in the DOM.

This is the functionality we are going to test…

First Test…

Let’s start by looking at how unit tests tend to organically grow. For our first test, I want to make sure that an “edit” button is visible in a row next to a user, when my currently logged in user has “edit” permissions.

My basic test structure looks like this:

user-view.spec.js

'use strict';

var UsersView = require('./user-view'); // the view we are testing
var UserModel = require('../models/user'); // the data models that are rendered in the table

describe('UsersView', function() {
  describe('user edit button', function() {
    it('shows when current user can edit accounts', function() {
    });
  });
});

To test this, I need to render a view with at least 1 user in the dataset so that the table of users will have a row containing the “edit” button. In Marionette, I can do this by “new”ing up on instance of the View and giving it a DOM element to render itself into.

I also need to pass an array of User objects in as the model property to my view instance.

Because I need to render my view into a DOM element, I will need to add a <div> element to the body of the page, then remove it after the test. Both Mocha and Jasmine provide a beforeEach() and afterEach() that can be used to execute some common code before and after each test. I am going to leverage these to set up and tear down this test DOM element.

describe('UsersView', function() {
  describe('user edit button', function() {
    var testElement;

    beforeEach(function() {
      // add an element to the page to render into
      testElement = $('<div></div>)';
      $('body').append(testElement);
    });

    it('shows when current user can edit accounts', function() {
      // Set up the currently logged in user
      Application.currentUser = new UserModel{
        id: 1,
        email: 'admin@myapp.com',
        first_name: 'FirstOne',
        last_name: 'LastOne',
        user_role: {
          id: 1,
          name: 'Admin',
          permissions: ['edit']
        }
      });

      // Set up the user data to be rendered
      var usersCollection = new Backbone.Collection();
      usersCollection.add(new UserModel({
        id: 2,
        email: 'someone@myapp.com',
        first_name: 'First',
        last_name: 'Last',
        user_role: 2
      }));

      // Set up the view to test
      var view = new UsersView({
        $el: testElement, // the element to render the view into
        model: usersCollection
      });

      // actually render the view into the DOM
      view.render();

      // assert that there is 1 visible edit button on the page.
      // edit buttons have the attribute data-action="edit" so we can jQuery select on that.
      expect(testElement.find('tbody tr [data-action=edit]').length).to.equal(1);
    });

    afterEach(function() {
      // clean up the test element after each test
      testElement.remove();
      testElement = undefined;
    });
  });
});

That is all there is for that test. Look at how much code there is here though:

  • 23 lines of setup (not including before/afterEach)
  • 1 line to act (render)
  • 1 line to assert

That’s a lot of setup!

The Second Test … Unclean!

So now we also want to test that the same button is not visible when the user does not have the “edit” permission. Guess how this usually happens in real-life day-to-day programming… yes, we copy/paste the first test and edit what we need, of course!

describe('UsersView', function() {
  describe('user edit button', function() {
    var testElement;

    beforeEach(function() {
      // add an element to the page to render into
      testElement = $('<div></div>)';
      $('body').append(testElement);
    });

    it('shows when current user can edit accounts', function() {
      // Set up the currently logged in user
      Application.currentUser = new UserModel{
        id: 1,
        email: 'admin@myapp.com',
        first_name: 'FirstOne',
        last_name: 'LastOne',
        user_role: {
          id: 1,
          name: 'Admin',
          permissions: ['edit']
        }
      });

      // Set up the user data to be rendered
      var usersCollection = new Backbone.Collection();
      usersCollection.add(new UserModel({
        id: 2,
        email: 'someone@myapp.com',
        first_name: 'First',
        last_name: 'Last',
        user_role: 2
      }));

      // Set up the view to test
      var view = new UsersView({
        $el: testElement, // the element to render the view into
        model: usersCollection
      });

      // actually render the view into the DOM
      view.render();

      // assert that there is 1 visible edit button on the page.
      // edit buttons have the attribute data-action="edit" so we can jQuery select on that.
      expect(testElement.find('tbody tr [data-action=edit]').length).to.equal(1);
    });

    it('hidden when current user cannot edit accounts', function() {
      // Set up the currently logged in user
      Application.currentUser = new UserModel{
        id: 1,
        email: 'admin@myapp.com',
        first_name: 'FirstOne',
        last_name: 'LastOne',
        user_role: {
          id: 1,
          name: 'Admin',
          permissions: [] // THIS LINE CHANGED. Removed "edit" permission.
        }
      });

      // Set up the user data to be rendered
      var usersCollection = new Backbone.Collection();
      usersCollection.add(new UserModel({
        id: 2,
        email: 'someone@myapp.com',
        first_name: 'First',
        last_name: 'Last',
        user_role: 2
      }));

      // Set up the view to test
      var view = new UsersView({
        $el: testElement, // the element to render the view into
        model: usersCollection
      });

      // actually render the view into the DOM
      view.render();

      // assert that there is 1 visible edit button on the page.
      // edit buttons have the attribute data-action="edit" so we can jQuery select on that.
      expect(testElement.find('tbody tr [data-action=edit]').length).to.equal(0); // THIS LINE CHANGED. Changed expected from 1 to 0.
    });

    afterEach(function() {
      // clean up the test element after each test
      testElement.remove();
      testElement = undefined;
    });
  });
});

Great! we now have 2 perfectly valid, passing tests. There are only 2 lines different between the tests! In reality, the same 2 tests are again duplicated but for the “delete” button, and again for the “create” button, so imagine we’ve copy/pasted this test 6 times and made minor changes to each.

Are these tests maintainable? or the even better question; How long does it take you to read and comprehend each test?

Nested beforeEach and afterEach.

Let’s make these tests actually respectable now. An important part of this is understanding that the beforeEach() and afterEach() functions can be provided for each describe(), including nested ones, PLUS a top-level pair that are not in a describe() at all.

When before/afterEach are in nested describes, they will all be executed in order of depth. As a very simple example, consider:

beforeEach(function() {
  console.log('global');
});

describe('depth1', function() {
  beforeEach(function() {
    console.log('depth1');
  });

  describe('depth2', function() {
    beforeEach(function() {
      console.log('depth2');
    });

    it('test 1', function() {
      console.log('test1');
    });

    it('test 2', function() {
      console.log('test2');
    });
  });
});

The result would be:


global
depth1
depth2
test1
global
depth1
depth2
test2

The second key is to understand that the this function context is set to an object that is passed to all of the beforeEach, afterEach, it functions. They all share the same “this”, and the object is unique to each test run.

Test Fixtures

We can build up a test fixture that is specific to each nested block of tests by pushing commonalities up the chain of nested beforeEach() setups.

The actual “fixture” will be available to each test via the this variable. Since “this” is set to an object that is shared by all the beforeEach(), it() and afterEach() calls, we can build up the fixture to contain the setups and teardowns needed for a specific block of tests.

Refactor to Create Fixtures (Finally, the good part!)

In this application, there are a lot of views that will need to all be tested, and a lot of code that deals with having a current user. We can refactor these out to a separate global-level setup/teardown file:

application-test-fixture.js

'use strict';

function _setupTestElement(fixture) {
  fixture.testElement = $('<div></div>)';
  $('body').append(fixture.testElement);
}

function _teardownTestElement(fixture) {
  fixture.testElement.remove();
}

function _setupCurrentUser() {
  window.Application.currentUser = new UserModel{
    id: 1,
    email: 'admin@myapp.com',
    first_name: 'FirstOne',
    last_name: 'LastOne',
    user_role: {
      id: 1,
      name: 'Admin',
      permissions: []
    }
  });
}

function _teardownCurrentUser() {
  delete window.Application.currentUser;
}

beforeEach(function() {
  _setupCurrentUser();
  _setupTestElement(this);

  // can be used by a test to set the current permissions for the logged in user.
  this.setCurrentUserPermissions = function(permissionsArray) {
    window.Application.currentUser.set('user_role.permissions', permissionsArray);
  }
});

afterEach(function() {
  _teardownTestElement(this);
  _teardownCurrentUser();
});

Then back in the test spec file, we can continue to build up a fixture:

users-view.spec.js

describe('UsersView', function() {
  beforeEach(function() {
    var fixture = this;

    fixture.createSingleUserCollection = function() {
      var collection = new Backbone.Collection();
      collection.add(new UserModel({
        id: 2,
        email: 'someone@myapp.com',
        first_name: 'First',
        last_name: 'Last',
        user_role: 2
      }));
      return collection;
    };

    fixture.renderView = function(data) {
      fixture.view = new UsersView({
        $el: fixture.testElement,
        model: data
      });
      fixture.view.render();
    };
  });

  describe('user edit button', function() {
    beforeEach(function() {
      var fixture = this;

      fixture.numberOfEditButtons = function() {
        return fixture.testElement.find('tbody tr [data-action=edit]').length;
      };
    });

    it('shows when current user can edit accounts', function() {
      // Arrange
      var fixture = this,
          users = fixture.createSingleUserCollection(),
          numberOfEditButtons = fixture.numberOfEditButtons;
      fixture.setCurrentUserPermissions(['edit']);

      // Act
      fixture.renderView(users);

      // Assert
      expect(numberOfEditButtons()).to.equal(1);
    });

    it('hidden when current user cannot edit accounts', function() {
      // Arrange
      var fixture = this,
          users = fixture.createSingleUserCollection(),
          numberOfEditButtons = fixture.numberOfEditButtons;
      fixture.setCurrentUserPermissions([]);

      // Act
      fixture.renderView(users);

      // Assert
      expect(numberOfEditButtons()).to.equal(0);
    });
  });
});

Wow! Aren’t these tests a lot easier to understand? We now just have 2 lines of setup, 1 line of action, and 1 line of assertion. And look at how cleanly the assertion reads! “Expect number of edit buttons to equal 0.” Could it be any more clear what this test is really doing?!

In addition, all the nuances of setting up parts of the test as well as the “magical” jQuery selectors are now pushed away to a place where we don’t have to repeat them for every test. If the selectors change, we can now change them in just 1 place. This is a huge improvement to test maintainability!

Advertisements
Tagged with: , , ,
Posted in JavaScript, Programming

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

CodingWithSpike is Jeff Valore. A professional software engineer, focused on JavaScript, Web Development, C# and the Microsoft stack. Jeff is currently a Software Engineer at Virtual Hold Technologies.


I am also a Pluralsight author. Check out my courses!

%d bloggers like this: