Javascript – Respectable design pattern for making node modules flexible/testable

design-patternsjavascriptnode.jsunit testing

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.

Best Answer

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();
Related Topic