6

I am looking to get some input from some more experienced testers than what I am. :)

I am trying to make my node modules testable, allowing for dependency spying/stubbing/mocking without the need to use a magic library such as rewire or mockery to intercept my module import/require statements.

I was thinking of using an approach whereby I would create a factory function within each module. The factory function accepts the required dependencies for the module to export. The default export would call the factory function using the standard import statements from the module. I would also export the factory function itself allowing for my tests to inject whatever dependencies they like.

Here is an example (which is completely contrived and simplified to highlight the design approach):

import { foo } from './utils/foo';

// We export our factory, exposing it to consumers.
export const factory = (dependencies = {}) => {
  const {
    $foo = foo // defaults to standard imp if none provided.
  } = dependencies;  

  return function bar() {
    return $foo();
  }
}

// The default implementation, which would end up using default deps.
export default factory();

Thoughts? Is this too verbose? Is there a better mechanism of achieving the same? I am slightly concerned about the boiler plate additions that I will be adding to my project, but perhaps the trade off is worth it.

4

In general, something like this is a good idea. I'm not very experienced with JavaScript in particular, but have loads of experience testing other languages. And there I have found that exposing callback hooks like this is an extremely valuable technique to allow dependency injection without drastically changing the overall architecture – in fact, I recently wrote an article suggesting this style of dependency management for C++ applications. My workflow goes something like this:

  • find the relevant dependencies. Not every dependency needs to be mocked, especially if they can be tested independently. However, do extract those where you'd want to inject data, insert tests spies, or mock away expensive operations.

  • describe the service provided by the dependency. You are probably not using the whole API provided by the dependency, but only a small subset. What are the services provided by that dependency? It can make sense to capture this description in a simple wrapper object. In the simplest case, the dependency is just on a single method call, in the more general case the service you want to extract is a factory function. The problem with factories is that you include the whole API of their product in your API (see the Law of Demeter for a discussion of this)

  • extract the dependency into an externally swappable callback. The default value just delegates to your dependency. How you access that callback can depend on a variety of factors. I'm a fan of passing the callback into each function that depends on it as an optional argument, and using the default callback is none is provided. However, a global variable can also work. A compromise is describing the whole API of your module explicitly as an object (see also the Facade Pattern). This avoids global dependency management, but makes it effectively global for your module.

  • override the dependency in tests.

The main difference between my approach and your code is that I'd do away with the barFactory generator, though I'm not sure I understand this ES6 code correctly. In plain old JavaScript with the revealing module pattern, I'd include an object describing all dependencies in the module facade:

var module = (function(){
  var deps = {};
  deps.frobnicateMyThing = function(thing) {
    staticDependency.frobnicate(thing);
  };

  function externallyVisibleApi() {
    ...
    deps.frobnicateMyThing(thing);
  }

  return {
    "externallyVisibleApi": externallyVisibleApi,
    "deps": deps,
  };
})();

...
// in test:
module.deps.frobnicateMyThing = function(thing) {
  assertIGotTheExpected(thing);
};

module.externallyVisibleApi();
  • You raise extremely good points about what dependencies I wish to expose, and what parts of their APIs are really relevant to my code. I am definitely going to take this into consideration. Off to read your article now... Thanks for this valuable input! – ctrlplusb Feb 8 '16 at 12:11
1

After trying several different approaches, I like to write very object oriented node modules. Basically borrowed from Java and the likes, a file is a module is a class.

// MyModule.js
class MyModule {

    // provide dependencies in constructor
    constructor(dependencyA, dependencyB) {
        this.dependencyA = dependencyA;
        this.dependencyB = dependencyB;
    }

}

module.exports = MyModule;


// MyModuleTest.js
var MyModule = require("./MyModule");

describe("MyModule Test", function () {

    it("should do something", function () {
        var mockA = {};
        var mockB = {};
        var myModule = new MyModule(mockA, mockB);

        // work with myModule and mocks
    });

});

This has several benefits:

  • It makes testing and mocking trivially easy
  • You can use real dependencies for Integration tests just as easy
  • There is no global state in tests, you can always work with fresh instances and seperate objects as mocks. The traditional approach in JavaScript is to create stubs or spies on "global" objects. This introduces global state, which is really dangerous and error prone (in my experience).
  • You can use an IOC Container with constructor injection for production

An IOC example with yaioc (disclaimer: I'm the author):

// myapp.js
var container = yaioc.container();
container.register(require("./DependencyA");
container.register(require("./DependencyB");
container.register(require("./MyModule"));
// ...
var myModule = container.get("myModule");

I used this for several projects and am happy with the great flexibility and simplicity in tests. There is little overhead and no complicated mocking/resetting/magic to remember.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.