tnwinc / Isolate

Isolate is a tool to allow injection of module dependencies when doing Test Driven Javascript/Coffeescript development.
44 stars 3 forks source link

Isolate Build Status

Isolate is a tool to allow injection of module dependencies when doing Test Driven Javascript/Coffeescript development. It extends requirejs and node's require to allow isolation of a module under test.

Paired with a Spy/Fake/Mock framework, this allows for a powerful TDD environment where the code under test is properly isolated from its dependencies.

Isolate can be used with either requirejs (AMD) or node's require (CommonJs). It works by hijacking the loader cache in either environment and injecting alternate stand-ins for each of the cached modules.

Why?

When building large or complex Javascript applications, modularity and testing is key. With these two ingredients alone, though, you quickly reach a point where a small mistake in one module can easily lead to large swaths of your tests failing because each module, to some degree, relies on its dependencies to function as expected.

If you instead isolate each module in its test environment with standin implementations of its dependencies, breaking the operation of one module only breaks its related tests. This allows you to develop with more confidence and track down issues faster.

Tools that go well with Isolate

Examples

Since code is worth 1024 words, here is a quick overview of what using Isolate in your code looks like.

requirejs / AMD

path/to/module.under.test.coffee

define ['path/to/dependency'], (dependency)->
  # implementation

path/to/depencency.coffee

define [], ->
  someMethod: -> # some logic

isolate configuration

isolate.map 'path/to/dependency',
  someMethod: -> true

spec file

define ['isolate!path/to/module.under.test'], (moduleUnderTest)->
  dependency = moduleUnderTest.dependencies['path/to/dependency']
  dependency.someMethod() # true

node / CommonJS

path/to/module.under.test.coffee

dependency = require 'path/to/dependency'
# implementation

path/to/depencency.coffee

exports.someMethod = -> # some logic

isolate configuration

isolate.map 'path/to/dependency',
  someMethod: -> true

spec file

moduleUnderTest = module.isolate 'path/to/module.under.test'
dependency = moduleUnderTest.dependencies['path/to/dependency']
dependency.someMethod() # true

Installation

npm install isolate

If you are using requirejs in the browser, Isolate integrates via their Loader Plugins API. You should place isolate.js "in the same directory as your app's main JS file."

Configuration

Isolate maps real module dependencies to fake implementations. It does this via mapping rules that you provide in its configuration. For small projects, you may be able to configure Isolate "just in time", right in your spec file - though it is much more maintainable to instead configure it during the bootstrap phase of running your specs.

For instance, if you are using mocha, you could put your isolate configuration in test/configure-isolate.js and add something like the following to your test/mocha.opts file:

--require test/configure-isolate.js

The test/configure-isolate.coffee file will be similar to:

global.Isolate = require 'Isolate'

Isolate
  .mapType #...
  .map #...
  .passthru #...
  .mapAsFactory #...

Configuration Options

useRequire
Isolate.useRequire require

useRequire allows you to specify the instance of require.js require to use when isolating AMD modules. If this is not set, Isolate will try to use the global require instance by default.

passthru
Isolate.passthru 'jquery', 'underscore', /lib\/.*/, '/libraries\/.*/' #...

or

Isolate.passthru [ 'jquery', 'underscore', /lib\/.*/, '/libraries\/.*/' ]

passthru allows you to specify that certain modules should be allowed through without injecting a standin. This is good for external libraries that are assumed to be working and stable, or are too complex to realistically build suitable standins for.

This option expects a list of matchers, and can be called multiple times. A matcher can be one of:

map
Isolate
  .map('some/module', {})
  .map('/.*_controller$/', (options)-> {})
  .map(/.*_view/, (options)-> {})

or

Isolate
  .map
    'some/module'      : {}
    '/.*_controller$/' : (options)-> {}
    '/.*_view/'        : (options)-> {}

map allows you to provide a specific standin implementation to inject for any given matcher (See the passthru section above for details on matchers).

This option expects to be provided a matcher and either a standin instance to inject, or a factory (see map.asFactory below). As syntactic sugar, you can also pass an object map of matcher: standin pairs too (second example above)

Note: Conflicts between passthru, map, and overlapping matchers of each are resolved by choosing the last-defined matching rule.

mapType
Isolate
  .mapType 'function', ->
  .mapType 'object', {}

or

Isolate
  .mapType
    'function': ->
    'object'  : {}

mapType allows you to setup "catch-all" rules to construct standins for modules which failed to match any map or passthru rules defined.

This option expects to be provided a type argument,and either a standin instance to inject, or a factory (see map.asFactory below). As syntactic sugar, you can also pass an object map of type: standin paris too (second example above).

The type argument is compared (case insensitive) to the output of running Object.prototype.toString on the actual module implemenntation. Just the substring containing the type is compared, so for a dependency which is a function, Object.prototype.toString would return [object Function] which means you should specify 'function' as the type to map.

isolateComplete
Isolate.isolateComplete (module)->

isolateComplete allows you to perform last-minute processing of a module before it is injected to the requesting code. The module reference has the dependencies property already prepared.

Mapping to factories instead of literals

Usage as a rule mapper
Isolate
  .mapAsFactory 'some/module',
    (actual, module_path, requesting_module_path)-> {}
  .mapAsFactory '/.*_controller$/',
    (actual, module_path, requesting_module_path)->
      (options)-> {}
  .mapAsFactory /.*_view/,
    (actual, module_path, requesting_module_path)->
      (options)-> {}

or

Isolate
  .map
    'some/module'      : (actual, module_path, requesting_module_path)-> {}
    '/.*_controller$/' : (actual, module_path, requesting_module_path)->
                           (options)-> {}
    '/.*_view/'        : (actual, module_path, requesting_module_path)->
                           (options)-> {}

mapAsFactory allows you to provide a dynamically generated standin implementation to inject with the possibility to customize the standin to the requested module details. mapAsFactory follows the same matcher rules described in the passthru section above.

This option expects to be provided a matcher and a function which generates the standin to inject. The function is provided 3 parameters

As syntactic sugar, you can also pass an object map of matcher: standin pairs too (second example above)

Note: Conflicts between passthru, map, and overlapping matchers of each are resolved by choosing the last-defined matching rule.

Usage as a modifier to map and mapType
Isolate
  .map /.*_controller/, Isolate.mapAsFactory (actual_module, module_path, requesting_module_path)->
    toString: -> "[Fake for #{module_path}]"
  .mapType 'function', Isolate.mapAsFactory (actual_module, module_path, requesting_module_path)->
    fake_function = ->
    fake_function.toString = -> "[Fake Function for #{module_path}]"
    return fake_function

map.asFactory can also be used to provide a factory function to map and mapType. The factory function will be evaluated when resolving the dependency. The parameters passed to the factory function are the same as described in the Usage as a rule mapper.

Note: mapAsFactory is very helpful when you want to inject a standin for adding some surface area to a module for specs (like wrapping functions in spies), but you still want to check that the integration between the modules hasn't been broken.

Using Isolate in tests

requirejs / AMD

node / CommonJS