Skip to content

biril/jasq

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

83 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Jasq

Build Status Bower version

AMD dependency injector integrated with Jasmine.

Jasq simplifies testing AMD modules by overloading Jasmine's describe and it to

  • maintain spec atomicity, avoiding persistent module state
  • allow mocking of the tested module's dependencies, per suite or per spec

Jasq is built on the assumption that any Jasmine suite will concern (and thus, test / define the specs for) nothing more than a single module. The Jasq version of describe allows specifying the tested module (by name) and ensures that it is made available to all contained specs, defined with it. These gain access to the tested module (through a module argument) and may easily provide ad-hock mocks for any and all of its dependencies. The tested module is reloaded per spec and uses any mocked dependencies defined. Mocks may also be defined at the suite level, to be reused for all contained specs.

To implement dependency injection, Jasq wraps Jasmine's describe & it global functions and additionally provides overloaded versions which differ in the parameters they accept. These act as extentions to Jasmine's built in functionality to be used as (and if) needed:

// Invoke 'describe' - do not associate a module which the suite
describe("My suite", function () {
  // .. Jasmine specs ..
});

// Invoke 'describe' passing a moduleName argument to associate the suite
//  with the module. Contained specs gain access to the module
describe("My suite", "tested/module/name", function () {
  // .. specs ..
});

// Invoke 'describe' passing a suiteConfig hash as a second argument. Allows
//  specifying mocks through suiteConfig.mock
describe("My suite", {
  moduleName: "tested/module/name",
  mock: function () {
    // Return a hash of mocked dependencies
  },
  specify: function () {
    // .. specs ..
  }
});

// Invoke 'it'
it("should do something", function () {
  // .. expectations ..
});

// Invoke 'it' expecting a module. Specs defined within a suite associated
//  with a module will receive one. Additionally they will receive the
//  module's dependecies
it("should do something", function (module, dependencies) {
  // .. expectations ..
});

// Invoke 'it' passing a specConfig hash as a second argument. Allows
//  specifying mocks through specConfig.mock
it("should do something", {
  mock: {
    // Define mocked dependencies
  },
  expect: function (module, dependencies) {
    // .. expectations ..
  })
};

Jasq uses RequireJS for loading modules and is compatible with Jasmine, versions >= 2.0.0. The current revision is tested against Jasmine v2.0.2 and only in a browser environment - support for Node is work in progress.

The following examples, while not a complete reference, should cover all essential use cases for Jasq. Further insight may be gained by taking a look the included example, the project's test suite and - of course - the source. For the latter, an annotated version is also maintained. Also, please consider not using it.

Jasq by example

Consider modules modA, modB where the latter is a dependency of the former:

// Defined in ModB.js:
define(function () {
  return {
    getValue: function () {
      return "B";
    }
  };
});

// Defined in modA.js:
define(["modB"], function (modB) {
  return {
    getValue: function () {
      return "A";
    },
    getValueAfterAWhile (cb) {
      setTimeout(function () {
        cb("A");
      }, 100)
    },
    getModBValue: function () {
      return modB.getValue();
    }
  };
});

A test suite for modA should be defined as a module hosting the relevant specs. It should require Jasq (but not the tested modA itself):

define(["jasq"], function () {
  // Implement modA test suite
});

Define the test suite by invoking describe, passing the module name as an additional parameter:

require(["jasq"], function () {
  // The name of the tested module - 'modA' - is passed as a 2nd parameter
  //  to the describe call
  describe("The modA module", "modA", function () {
    // Implement modA specs
  });
});

This will make the module available to all specs within the suite as the expectation-function passed to any nested it will now be invoked with modA as an argument:

require(["jasq"], function () {
  describe("The modA module", "modA", function () {

    // The module is passed to specs within the suite, as a parameter
    it("should have a value of 'A'", function (modA) {
      expect(modA.getValue()).toBe("A"); // Passes
    });
  });
});

Note that the module will also be available to specs within nested suites:

require(["jasq"], function () {
  describe("The modA module", "modA", function () {

    describe("its value", function () {

      // The module is also passed to specs within the nested suite
      it("should be 'A'", function (modA) {
        expect(modA.getValue()).toBe("A"); // Passes
      });
    });
  });
});

Additionally, Jasq ensures that module state will not be persisted across specs:

require(["jasq"], function () {
  describe("The modA module", "modA", function () {

    // This spec modifies modA
    it("should have a value of 'C' when tweaked", function (modA) {
      modA.getValue = function () {
        return "C";
      };
      expect(modA.getValue()).toBe("C"); // Passes
    });

    // This spec is passed the original, unmodified modA
    it("should have a value of A", function (modA) {
      expect(modA.getValue()).toBe("A"); // Passes
    });
  });
});

To mock modA's dependencies, invoke it passing a specConfig hash as a second argument. Use the specConfig.mock property to define a mapping of dependencies (module names) to mocks, as you see fit. Pass the expectations function through the specConfig.expect property. In the following example, modB is mapped to a mockB object:

require(["jasq"], function () {
  describe("The modA module", "modA", function () {

    // Define a mock for modB
    var mockB = {
      getValue: function () {
        return "C";
      }
    };

    // modA will use the mocked version of modB
    it("should expose modB's value", {
      mock: {
        modB: mockB
      },
      expect: function (modA) {
        expect(modA.getModBValue()).toBe("C"); // Passes
      }
    });
  });
});

Specs additionally receive a dependencies argument which may be used to directly access any mocked dependencies:

require(["jasq"], function () {
  describe("The modA module", "modA", function () {

    // Mocked modB may be accessed through 'dependencies.modB'
    it("should expose modB's value", {
      mock: {
        modB: {} // Mocking with an empty object
      },
      expect: function (modA, dependencies) {
        dependencies.modB.getValue = function () {
          return "D";
        };
        expect(modA.getModBValue()).toBe("D"); // Passes
      }
    });
  });
});

Often, it may be useful to access a dependency without necessarily creating a mock beforehand. The dependencies hash may be used to access any dependency, mocked or not:

require(["jasq"], function () {
  describe("The modA module", "modA", function () {

    // modB may be accessed through 'dependencies.modB'
    it("should delegate to modB to expose modB's value", function (modA, dependencies) {
      spyOn(dependencies.modB, "getValue");
      modA.getModBValue();
      expect(dependencies.modB.getValue).toHaveBeenCalled(); // Passes
    });
  });
});

In cases where multiple specs make use of the very same mocks, you can avoid repeating their definitions per spec by providing a 'mocking function' at the suite level. The mocking function should instantiate all needed mocks and return a hash that maps them to dependencies (module names). This will make the mocks available to all specs defined within the suite. Note that the mocking function will be invoked - and the mocks will be re-instatiated - per spec.

To do this, describe should be invoked with a suiteConfig hash as a second argument. Use the suiteConfig.mock property to pass the mocking function. Assign the name of the tested module to suiteConfig.moduleName and the specs function to suiteConfig.specify:

require(["jasq"], function () {
  describe("The modA module", {
    moduleName: "modA",
    mock: function () {

      // Define a mock for modB
      return {
        modB: {
          getValue: function () {
            return "C";
          }
        }
      };
    },
    specify: function () {

      // modA will use the mocked version of modB
      it("should expose modB's value", function (modA) {
        expect(modA.getModBValue()).toBe("C"); // Passes
      });

      // This spec modifies the mocked modB
      it("should not cache modB's value", function (modA, dependencies) {
        dependencies.modB.getValue = function () {
          return "D";
        };
        expect(modA.getModBValue()).toBe("D"); // Passes
      });

      // modA will use the mocked version of modB, unmodified
      it("should expose modB's value - again", function (modA) {
        expect(modA.getModBValue()).toBe("C"); // Passes
      });
    }
  });
});

Note that mocks defined at the suite level will be overriden by those defined in specs:

require(["jasq"], function () {
  describe("The modA module", {
    moduleName: "modA",
    mock: function () {

      // Define a mock for modB
      return {
        modB: {
          getValue: function () {
            return "C";
          }
        }
      };
    },
    specify: function () {

      // Redefine the modB mock - modA will use the redefined version
      it("should expose modB's value", {
        mock: {
          modB: {
            getValue: function () {
              return "D";
            }
          }
        },
        expect: function (modA) {
          expect(modA.getModBValue()).toBe("D"); // Passes
        }
      });
    }
  });
});

Asynchronous specs which are associated with a module, or are part of a suite associated with a module, can access Jasmine's done function as the third argument. For specs which aren't, done can be accessed as the first (and only) argument:

require(["jasq"], function () {

  // If spec is associated with a module access 'done' as the third argument
  describe("The modA module", "modA", function () {

    it("should have a value of A, after a while", function (modA, dependencies, done) {
      modA.getValueAfterAWhile(function (value) {
        expect(value).toBe("A"); // Passes
        done(); // Invoked to start the spec
      });
    });
  });

  // Otherwise access 'done' as the first (and only) argument
  describe("Something", function () {

    it("should happen after a while", function (done) {
      setTimeout(function () {
        done(); // Invoked to start the spec
      }, 100);
    });
  });
});

Set up

bower install jasq to obtain the latest Jasq plus dependencies. If you prefer to avoid bower, just include jasq.js in your project along with RequireJS.

For a typical example of test-runner configuration please take a look a the included example test suite.

Now, don't use it

As an aside, it's worth noting that suites making use of Jasq will incur some added overhead. Reloading the relevant module(s) per spec relies on appending <script> tags into the document as the suite executes. Additionally it introduces an extra layer of asynchronicity as every spec essentially becomes async. There's a also a price to pay in terms of congitive load as Jasq suites are arguably less readable than their plain 'ol Jasmine counterparts.

However, regardless of specifics concerning performance, it's worth noting that Ideally a project should not rely on any form of injection at the module loader level - even if only for the purposes of testing. The opposite may often be a result of less than optimal design choices presenting as testing impediments. In particular, the value in Jasq's spec-atomicity and dependency-mocking features is often rooted in such patterns as (ab)use of singletons and tight coupling between components.

Indeed, the impracticality of reverting singleton instances to some known state after their initial instantiation, leads to the need for their containing modules to be re-loaded per test - if those tests are to be atomic. The same can be said for any module which has side effects - introduces state - at load-time. Contrary to this, non-singleton components that don't 'leak state' may simply be loaded once and instantiated anew, per test.

Similarly, a requirement for dependency injection at the module loader level may be indicative of tightly coupled components within the application. A DI pattern at the application level will mitigate tight coupling and allow for tests where mocks can be injected in a straightforward manner (e.g. as constructor parameters) without the loader's involvement.

Having said that, there's valid use-cases for Jasq. For example tight coupling may be unavoidable within design constraints that lie outside the author's control. Such as those introduced by the use of a specific framework. Authors may choose to leverage Jasq on a per-unit basis, rather than across the entirety of the project's test suite, minimizing the relevant overhead to where necessary.

Testing / Contributing

The QUnit test suite may be run in a browser (test/test.html) or on the command line, by npm test. Note that the latter requires an installation of phantomjs.

Contributions are obviously appreciated. Please commit your changes on the dev branch - not master. dev is always ahead, contains the latest state of the project and is periodically merged back to master with the appropriate version bump. In lieu of a formal styleguide, take care to maintain the existing coding style. Please make sure your changes test out green prior to pull requests.

License

Licensed and freely distributed under the MIT License (LICENSE.txt).

Copyright (c) 2013-2014 Alex Lambiris