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:
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 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 });
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.
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 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:
App.Models.Note
objectsTransforming 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.
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.
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.
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:
modification
suite, add an initial App.Models.Note
object to the collection and wipe the collection after each testThe 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
.
44.220.184.63