1

I have an ES6 class which depends on some external modules to work. As this is a node application I use CommonJS to require and load the modules.

However it is no secret that this kind of module loading makes unit testing complicated. I could of course dependency inject all required modules via the constructor, however this feels cumbersome in a dynamically typed language. I also don't like using libraries like proxyquire as it bloats up my test code.

So I came up with the idea to store required modules as instance variables. For example:

const someModule = require('some-module');

class MyClass {

    constructor() {
        this.someModule = someModule;
    }

    someFunction(value) {
        return this.someModule.someFunction(value);
    }

}

This way I can load the dependencies using module loaders and still spy/stub/mock them in my unit tests.

Is this considered bad practice or can you see any major disadvantages?

benjiman
  • 3,888
  • 4
  • 29
  • 44

1 Answers1

1

This is surely acceptable on case-by-case basis. Static or prototype someModule property will be more efficient, but on the other hand, this will require to restore it after mocking in tests.

On the regular basis this pattern may become cumbersome, in this case DI container may be more convenient. There are many of them in Node realm, e.g. injection-js that was extracted from Angular DI.

In its most simple form it can be a purely singleton container that doesn't create instances by itself but stores existing values (module exports) under random tokens:

class Container extends Map {
  get(key) {
    if (this.has(key)) {
      return super.get(key);
    } else {
      throw new Error('Unknown dependency token ' + String(key));
    }
  }

  set(key, val) {
    if (key == null) {
      throw new Error('Nully dependency token ' + String(key));
    } else if (arguments.length == 1) {
      super.set(key, key);
    } else {
      super.set(key, val);
    }
  }
}

const container = new Container;

The dependencies can be registered and retrieved directly from the container:

const foo = Symbol('foo');
container.set(foo, require('foo'));
container.set('bar', require('bar'));
container.set(require('baz'));
...
const { foo } = require('./common-deps');

class Qux {
  constructor() {
    this.foo = container.get(foo);
    ...
  }
}

Additionally, the injector can embrace the container:

class DI {
  constructor(container) {
    this.container = container;
  }

  new(Fn) {
    if (!Array.isArray(Fn.annotation)) {
      throw new Error(Fn + ' is not annotated');
    }

    return new Fn(...Fn.annotation.map(key => this.container.get(key)));
  }

  call(fn) {
    if (!Array.isArray(fn.annotation)) {
      throw new Error(fn + ' is not annotated');
    }

    return fn(...fn.annotation.map(key => this.container.get(key)));
  }
}

const di = new DI(container);

And take care of DI in annotated classes and functions (on annotation, see this explanation):

class Qux {
  constructor(foo, bar) {
    this.foo = foo;
    ...
  }
}
Qux.annotation = [foo, 'bar', require('baz')];

quuxFactory.annotation = [require('baz')]
function quuxFactory(baz) { ... }

const qux = di.new(Qux);
const quux = di.call(quuxFactory);
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • I'll accept your answer and give the DI frameworks a try. Thanks a lot for the detailed infos :) Some further info: I took a broader look into the various DI frameworks available for node and tried several of them. I found https://github.com/justmoon/constitute has a very clean and lightweight API (seems to be inspired by the Angular DI concept as well). – benjiman Jul 19 '17 at 18:42
  • You're welcome. Thanks for sharing, I'll check it. The author admits that it copies Aurelia DI for the most part, so you can check [its manual](http://aurelia.io/hub.html#/doc/article/aurelia/dependency-injection/latest/dependency-injection-basics/6) for some concepts. The good thing about injection-js is that it's supported and pretty well documented in Angular manual, also very straightforward for Angular fullstack development. – Estus Flask Jul 19 '17 at 19:18