Getting the application and tests ready to run

Now that we have the Chai assertion library API under our belts, it is time to write and organize the tests. While we have incidentally covered much of this material already, the concise core of a Mocha test infrastructure includes:

  • Test runner: Configures the overall test run and report
  • Test suites: One or more organization units grouping many specifications/tests
  • Setup/Teardown: Setting up a state for each test or suite run
  • Specifications: Writing the test functions

Starting at the highest level, we look at our test driver web page. As previously discussed, this is where our core application libraries, test libraries, and test specifications are set up and included. All of the Backbone.js application tests in the rest of this chapter are incorporated into the chapters/03/test/test.html driver page.

The Mocha test runner

The Mocha setup() function controls the overall parameters and environment for all test suite and specification executions. The function should be called once before execution starts (for example, mocha.run()) in the test driver web page:

mocha.setup("bdd");

window.onload = function () {
  mocha.run();
};

The default settings are quite usable for Backbone.js testing and we use the previous code for nearly all of the tests in this book. However, there are many more options available, which are described at http://visionmedia.github.io/mocha/. Here is an arbitrary sampling:

mocha.setup({
  ui: "bdd",          // BDD UI.
  reporter: "html",   // HTML reporter.
  timeout: 4000,      // 4s test timeout.
  slow: 1000          // A "slow" test is > 1s
});

Reconfiguring the application for testing

Backbone.js applications often need specific test-friendly configurations in order to make the test environment predictable and to avoid stomping on real data. Backend information (for example, host addresses and ports) is often different in the development and testing phases. Thus, it is a good practice to abstract all of this information into a common file so that we can easily switch values from one central location to another.

Tip

An alternative and complementary approach for creating a workable test environment is to fake out configuration details and dependencies with a library such as Sinon.JS. The two Sinon.JS abstractions that can help us here are stubs and mocks, both of which can replace object method behaviors during tests. (We will introduce and discuss these concepts in detail in Chapter 5, Test Stubs and Mocks.)

In the following datastore configuration examples, we could use a Sinon.JS stub to replace the entire datastore with test-specific simulation code. The stub would allow us to use normal application configurations while ensuring that we do not modify the real datastore. As an added benefit to this approach, stubbing and mocking external dependencies can often make tests run faster, particularly if the fake replaces a relatively slow application behavior (for example, network communication).

In the Notes application, we require a unique localStorage name for the collection, which we specify in the configuration file notes/app/js/app/config.js:

App.Config = _.extend(App.Config, {
  // Local Storage Name
  storeName: "notes"
});

This code populates the App.Config namespace with App.Config.storeName, which we then use for the App.Collections.Notes collection in notes/app/js/app/collections/notes.js:

App.Collections.Notes = Backbone.Collection.extend({
  model: App.Models.Note,

  // Sets the localStorage key for data storage.
  localStorage: new Backbone.LocalStorage(App.Config.storeName)
});

With this setup, the live application will save data to the notes store in localStorage. However, in our tests we will want to add, remove, and mutate the note data without overwriting our development-friendly datastore. So, by adding an extra configuration directive in our application's test driver page, we can set the test-only store name to notes-test using the Underscore.js extend() function:

<script src="../app/js/app/namespace.js"></script>
<script src="../app/js/app/config.js"></script>
<script>
  // Test overrides (before any app components).
  App.Config = _.extend(App.Config, {
    storeName: "notes-test" // localStorage for tests.
  });
</script>
<script src="../app/js/app/models/note.js"></script>
<script src="../app/js/app/collections/notes.js"></script>

By including config.js first and then overriding the specific values, we make the other unmodified configuration values available during the tests. With this scheme, we now have a completely separate Backbone.js test datastore, which we can change without affecting our development environment.

Organizing topics and specifications into test suites

Organizing test code into topics and application components is an important step in developing an overall test architecture. To this end, Mocha provides the describe() test suite function to group logical collections of test specifications.

For example, in App.Collections.Notes we might start with two subgroups of tests:

  • Tests that create empty collections and verify the initial default state
  • Tests that modify the collection with new App.Models.Note objects

Transforming this list into a set of nested Mocha test suites would give us:

describe("App.Collections.Notes", function () {

  describe("creation", function () {
    // Tests.
  });

  describe("modification", function () {
    // Tests.
  });
});

We have two levels of describe() statements here, although Mocha allows a much deeper nesting of suites.

Starting up and winding down tests

Although we try to isolate behavior for our test specifications as a general practice, tests often have common setup and teardown needs. For example, if a group of specs all test the same object, it may make the most sense to create the object once and share it with all of the specs.

Tip

Context/member variables in Mocha

Mocha allows test code to attach values to the this context object for use in other sections of the test run. This allows us to share variables across tests without having to declare and manage global or higher-level scoped variables. A common use of this feature is to add a variable such as this.myCollection in a before() setup statement for a group of tests and then remove it in the after() statement for the group.

Mocha provides the functions before(), beforeEach(), after(), and afterEach() to help us with test state management. As mentioned previously, the before()/after() functions run once before and once after all the tests within a suite. The beforeEach()/afterEach() functions run before and after each test within a suite.

With these four constructs, we can create nuanced state management for our Mocha tests. The setup/teardown functions operate at the level of each test suite. This means that nested test suites can provide their own additional setup/teardown functions. For example, Mocha will faithfully run each before() statement as it traverses deeper into the describe() statements before executing the first test.

Note

Another good reason to use the setup/teardown functions is that they always run—even when the test specs fail or throw exceptions. This prevents a single test failure from affecting the data state of other tests in the run and causing spurious test failures.

Backbone.js collection tests often benefit from using setup and teardown helpers to create an initial data state. Usually, this means adding some starting records (called data fixtures when loaded from a separate data file) and restoring the datastore to a pristine state after the tests have modified the collection.

In the test driver page, we have already configured the App.Collections.Notes class to use a test-only datastore. The Backbone.localStorage adapter has an internal method _clear() that clears the underlying browser storage associated with the collection, which we will use to reset our data state in the tests. The resulting data sandbox is ready for the following test scenario:

  • Wipe any existing collection data on suite setup, add a context variable for a single collection, and remove the collection on teardown
  • In the modification suite, add an initial App.Models.Note object to the collection and wipe the collection after each test

The implementation of the setup and teardown functions for the test suite looks like the following:

describe("App.Collections.Notes", function () {

  before(function () {
    // Create a reference for all internal suites/specs.
    this.notes = new App.Collections.Notes();

    // Use internal method to clear out existing data.
    this.notes.localStorage._clear();
  });

  after(function () {
    // Remove the reference.
    this.notes = null;
  });

  describe("creation", function () {
    // Tests.
  });

  describe("modification", function () {

    beforeEach(function () {
      // Load a pre-existing note.
      this.notes.create({
        title: "Test note #1",
        text: "A pre-existing note from beforeEach."
      });
    });

    afterEach(function () {
      // Wipe internal data and reset collection.
      this.notes.localStorage._clear();
      this.notes.reset();
    });

    // Tests.

  });
});

The highlighted lines in the previous code snippet illustrate the before()/after() calls for all tests in the overall test suite and the beforeEach()/afterEach() calls for only the subsuite modification.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
44.220.184.63