Does Dependency Injection Have a Place in JavaScript?

And if so, where can it be used to best effect?

Jamie Morris
Oct 2, 2019 · 8 min read
Photo by Edvard Alexander Rølvaag on Unsplash

Years ago, when testing my apps meant “clicking about a bit” in a browser, I recall talking to some more learned peers about dependency injection. A fellow called John was trying to explain the concept to me on a train, without the benefit of a codebase to point emphatically at. It sounded awfully clever but it didn’t sink in. I resolved to do some research since I was supposedly the lead developer at my small company and felt that I should know this stuff. It still didn’t really make sense to me, since it was a solution to a problem I didn’t have. I was not writing unit tests (or any kind of tests for that matter) and therefore did not need to mock dependencies, and therefore was not programming to interfaces, even in C# where Interfaces actually exist (unlike JavaScript).


What Is Dependency Injection?

Dependency Injection (sometimes referred to as DI, Inversion of Control, or IoC) can be summarised as the practice of separating configuration from implementation. That might mean literal configuration, such as the URL of an endpoint you want to hit — rather than hard-coding the URL into the class which talks to the endpoint, you might “inject” that URL by passing it to the constructor.

Or you might instead have a dependency on another class, such as having a ProductService class which writes log messages via a Logger class. In such an example, the Logger could be injected as a dependency by passing it to the constructor of ProductService. There are a number of reasons you may want to inject your dependencies in this way:

  • Logger is reusable, and can be injected as a dependency into CustomerService. Doing so avoids the duplication of code that exists in Logger.
  • In order to test only the business logic in ProductService, we can now easily provide a mocked implementation of Logger in our unit tests.
  • If we agree on the interface for Logger, then someone else can easily write the implementation while we get on with writing and testing ProductService. Adhering to single responsibility in this way allows us to work more efficiently in parallel.

Some of these points are more related to the principle of single responsibility rather than dependency injection. You could have Logger instantiated inside ProductService rather than injected into it. However, it would make ProductService harder to test.

One of the reasons I often hear as justification for dependency injection is that it allows you to write to interfaces and then change your implementation more easily. The thinking here is that if you have an interface for Logger it does not matter if you write your messages to a file, a database or the console. What matters is that Logger defines a contract that won’t change if you change the underlying implementation. This is useful for generic libraries like a Logger where you might want to write to a file for one app, but to a database for another app. If someone else provides the logging library, you can switch implementations easily. Outside of providing libraries for other developers, though, it’s rare that I have two implementations of the same interface that I want to use in the same project. When I changed from using SQL Server to MongoDB in an app I was writing, I simply changed the implementation of my DBService.

Testing alone, however, is a strong enough reason for me to want some kind of dependency injection. In languages like C# or Java, writing to an interface has long been a necessary way to provide mocks, although reflection alleviates some of this need nowadays. Still, writing to an interface is generally seen as good practice due to the ease with which you can provide a different implementation simply by updating your config. If this only ever happens in tests, so be it — that’s good enough for me.


JavaScript

class Logger {
log(message) {
console.log(message);
}
}

const fakeLog = (message) => {
console.log(`MOCK: ${message}`);
}

const x = new Logger();
x.log('Hello.');
x.log = fakeLog; // Change the behaviour of only this instance of the class
x.log('Hello again!'); // Should display "MOCK" prefix

// A new instance of the class still behaves as normal
const y = new Logger();
y.log('This is a new logger.');

// Change the implementation for every instance of this class
Logger.prototype.log = fakeLog;
x.log('Goodbye.');
y.log('Farewell');

If you run this code in your browser’s JavaScript console, you should see the following output:

Hello.
MOCK: Hello again!
This is a new logger.
MOCK: Goodbye.
MOCK: Farewell

The ability to override the prototype of a class is a powerful feature that means you don’t actually need Dependency Injection in order to mock your dependency. You could simply change the behaviour before you run your test, as this further example shows:

module.exports.Logger = class Logger {
log(message) {
console.log(message);
}
}
const Logger = require('./logger').Logger;

module.exports.ProductService = class ProductService {
constructor() {
this.logger = new Logger();
}
delete(id) {
this.logger.log(`Deleting product ${id}`);
}
}

If you run “node test.js” you will notice that the mock implementation of Logger.log is used, even though we didn’t inject anything. That is because we have changed the implementation for the Logger class, not just the instance used in ProductService.

From this standpoint, it’s not necessary to have Dependency Injection in JavaScript. Having already established that the most valuable result of Dependency Injection is testing (and by extension, mocking), we have now shown a way to mock our dependencies without injecting them.

However, there is a bit of a smell here. We are only able to override the implementation at the class level, meaning that our tests will no longer be encapsulated. This isn’t so much of an issue if we run our tests one after the other, since we can just change the behaviour wherever it is needed, but it puts the burden on the developer to change the implementation back when they are finished — neglecting to do so could cause unexpected behaviour in subsequent tests (especially the tests for the class you just mocked).

In addition, you may decide to run your tests in parallel in order to speed up execution. If two tests both override the same dependency at the same time, then weird things will start to happen. If you instead provide the dependency via the constructor, then you can mock the instance of the class rather than the prototype. Even better, don’t even touch the original class, but provide something that looks like it. For example, the following object looks like the Logger in our previous example as far as ProductService is concerned:

const mockLogger = {
log: message => {
console.log(`MOCK: ${message}`);
}
};

You might wonder why you would override the implementation like this, but in reality, you would be using a library like Sinon to provide spies instead of your real implementation, allowing you to assert that a particular method on your mock was called from your subject under test.

The benefit of this approach is that you won’t accidentally leave any real implementation in place because you forgot to mock its behaviour. Your tests will fail fast if your code calls a method you forgot to mock, rather than executing the real implementation, which could include API calls or similar behaviours you don’t want in your tests.


Frameworks

For example, if your entry method is A, which has a dependency on B, which has a dependency on C, which has a dependency on D and so on, you have to instantiate D when A is called so that you can instantiate C and thus B. Confused? You aren’t the only one. This is why there are so many Dependency Injection frameworks around. The idea behind these is to configure all of your dependencies in one place and then whenever you need an instance of a class, you just ask the framework to create one. It resolves the dependencies for you. Often you will only do this once, resolving your top level dependency, which will trickle down through all of its sub-dependencies resolving them so that you don’t have to worry about it. The benefit here is that if D later develops a dependency on E, you don’t have to go around adding E to every place that instantiates D — you just configure it in one place.

In JavaScript, you have a couple of options. If you are using the Angular framework, the good news is that this is all built into Angular’s module system. When your app starts, your root module wires up the dependencies you require throughout the app, and allows you to defer some of this dependency resolution to feature modules. In your tests, you do the same thing with the TestBed (which is just a module at a much smaller scale), providing only the dependencies you need to run the tests in this file / describe block. This is one of the reasons that I love the Angular framework, and why I found testing in React frustrating (despite the other things that I love about React). Jest gets around some of this by allowing you to mock imports, but has the major drawback that you can only provide one implementation per spec file. This means if you have three tests that each need to mock a different behaviour, you need to have three spec files instead of just one.

If you aren’t using Angular, there is a great framework called Inversify, which provides a different way of configuring your dependencies but the same level of control. I tend to use Inversify when I’m writing Node applications or scripts.

I won’t go into the detail of either of these frameworks — this piece is more about whether you need Dependency Injection than how to use a particular library. But frameworks for DI are a good thing since they reduce the amount of boilerplate you have to write and make your code easier to maintain. Also, there may be other options that I haven’t mentioned, so feel free to shout out to your favourite in the comments section.


Conclusion

As always, this is largely just opinion on my part. You may disagree with some or all of it — if so, let’s talk.

JavaScript in Plain English

Learn the web's most important programming language.

Jamie Morris

Written by

I used to be a full stack developer, but nowadays most people only want me for my <body>, so I try to be a UI expert. All opinons are my own, not my employer's.

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade