Testing the application: models and views

August 25, 2014

As part of the series of posts, the next three posts are dedicated towards writing tests for the Backbone application created in the previous post.

All of the tests (in more detail than outlined below) are available.

Adding the page to run the tests

As with the tests for the API layer, the application tests are written in Jasmine. To run the Jasmine specifications in an application context, a very basic page is used to load the dependencies and run the tests.

<meta charset="UTF-8">
<title>Example App Spec Tests</title>
<!-- Include the Jasmine assets for running in an HTML page. -->
<link rel="stylesheet" type="text/css" href="lib/jasmine/jasmine.css"></link>
<script data-main="test" src="../lib/require/require.js"></script>
<!-- Left intentionally blank. -->  

Defining and loading dependencies

Similar to the setup of the application itself, all of the dependencies and the tests themselves are loaded through require statements. As previously mentioned, the require configuration for the application was placed into an isolated file. That allows it to be re-used when loading the dependencies for the tests. A separate require configuration file exists to define the modules necessary for writing the tests. Both of these files are included along with boot, jasmine and jasmine-html.

var specs = [];
require(["../js/appRequireConfig.js"], function() {
  require(["./testRequireConfig.js"], function() {
    require(["boot"], function () {
      // Load the specs require(specs, function () {
        // Initialize the HTML Reporter and execute the environment (setup by `boot.js`)
        window.onload();
      });
    });
  });
});

Specifying the specifications to run

The test.js file contains a list of specifications to be run whenever the test page is loaded. This list of specifications is used when loading the dependencies (see above).

var specs = [
  //  Models
 "spec/models/planSearchSpec",
  
  // Views
  "spec/views/planSearchSpec",
];

As additional specifications are created, they must be added to this list.

Test structure

The tests are all structured in the same way. First, they include a define block referencing the application components necessary for the specification. Next, they contain a description of what is being tested.

define(["models/planSearch"], function( PlanSearch ) {
  describe("Model :: PlanSearch", function() {
    // ... Tests go here
  });
});

From that point, separate describe blocks are used to describe the individual functions being tested and enumerate the scenarios of the tests.

Adding model tests

For the models, testing the initialize function is fairly straightforward.

describe("initialize", function() {
  var options;
  beforeEach(function() {
    options = { workflow: "test" };
  });
  
  it("should copy the workflow from options", function() {
    var model = new PlanSearch({}, options);
    expect(model.workflow).toBe(options.workflow);
  });
});

Testing the model functions that interact with the workflow are more complicated. While there are some basic paths that can be tested, the scenarios that test workflow interaction will be deferred until a later post that specifically addresses spies and mocks.

Adding view tests

The initialize and render functions of the views can be tested without interacting with the rest of the functions defined in the view.

describe("initialize", function() {
  var model;
  beforeEach(function() {
    model = new PlanSearch();
  });
  
  it("loads a template", function() {
    var view = new PlanSearchView({model:model});
    expect(typeof view.template).toBe("function");
  });
});

describe("render", function() {
  var model;
  var view;
  beforeEach(function() {
    model = new PlanSearch();
    view = new PlanSearchView({model:model});
  });
  
  it("puts content into the element", function() {
    view.render();
    var rendered = $(view.el).html();
    expect(rendered).not.toBe("");
  });
});

For other functions, the view must first be rendered (in the beforeEach block in the example below). Most functions in the view other than initialize and render will involve some amount of access to (or manipulation of) elements that are not present until the view is rendered.

describe("updateModelFromView", function() {
  var model;
  var view;
  beforeEach(function() {
    model = new PlanSearch();
    view = new PlanSearchView({model:model});
    view.render();
  });
  
  it("copies the values from the DOM", function() {
    $(view.el).find("#policyNumber").val("PL123");
    $(view.el).find("#firstName").val("John");
    $(view.el).find("#lastName").val("Doe");
    
    view.updateModelFromView();
    
    expect(model.get("policyNumber")).toBe("PL123");
    expect(model.get("firstName")).toBe("John");
    expect(model.get("lastName")).toBe("Doe");
  });
});

Running the tests

To run the tests, navigate to the app/tests directory. If setup using the provided nginx configuration, this location would be /app/tests on port 9000.