Dependency Injection in Javascript for Beginners

Bertil Muth - Aug 19 '21 - - Dev Community

A few days ago, one of my students showed me his code. He had written an AWS lambda function that scrapes a web site and posts contents to Discord. He was unhappy because he couldn't test the contents of the messages being posted. He said that there wasn't a mocking framework for the external services.

I told him that he doesn't need a mocking framework. He just needs to use Dependency Injection (DI). DI enables you to:

  • test business logic isolated from external services and frameworks
  • switch services, technologies and frameworks more easily

Dependency Injection is at the heart of architectural styles like Clean Architecture and Hexagonal Architecture. Yet, you hardly find any simple examples that address DI specifically.

In this article, I will walk you through a simple example. Think of a calculator that adds and subtracts numbers and prints the results to the console:

add(5,3);
sub(100,1);

function log(result){
    console.log(result);
}

function add(x, y){
    const result = x + y;
    log(result);
}

function sub(x, y){
    const result = x - y;
    log(result);
}

Enter fullscreen mode Exit fullscreen mode

This is what the code prints to the console:

8
99
Enter fullscreen mode Exit fullscreen mode

The add and sub functions know the log function. Calculation and console logging are tightly coupled.

Think about that for a minute. What's the problem?

If you want to display the result on some other output channel, i.e. the GUI, you need to adapt the functions. The more output channels, the more complicated the functions become. Even though their main purpose is to calculate the result.

In your tests, you don't even want to print to the console. It only makes your tests slow. You just want to know if the result of the mathematical operation is correct.

So what can you do about it? How does DI help in the example?

You need to move the knowledge about the concrete display function out of add and sub. The simplest way to do that is to pass it as an argument. Here's the same example, but using DI. The output is the same as above.

add(5,3, log);
sub(100,1, log);

function log(result){
    console.log(result);
}

function add(x, y, display){
    const result = x + y;
    display(result);
}

function sub(x, y, display){
    const result = x - y;
    display(result);
}
Enter fullscreen mode Exit fullscreen mode

You pass in the log function as an argument to add and sub. These functions then call log by using display, like an alias. So in this code, display(result); is equivalent to log(result);.

Since add and sub no longer know the exact function for displaying, you can pass in other functions. Say that on top of logging, you want to show an alert to the user in the GUI. Here's the code for that:

add(5,3, log);
add(5,3, alert);

sub(100,1, log);
sub(100,1, alert);

function log(result){
    console.log(result);
}

function add(x, y, display){
    const result = x + y;
    display(result);
}

function sub(x, y, display){
    const result = x - y;
    display(result);
}
Enter fullscreen mode Exit fullscreen mode

We don't need to write code for alert. It's a built in Javascript function.

Finally, how do you approach testing? I'm not going into details of a testing framework. But here's the idea how to test with DI.

Using DI, you can pass in any function. It doesn't have to display. It can check whether the result is correct instead.
So here's a call that shows whether the result of 5 plus 3 equals 8:

add(5,3, r => alert(r == 8));
Enter fullscreen mode Exit fullscreen mode

The code passes an anonymous function as third argument. Also known as a lambda function. It could have been a named function instead - that doesn't matter.

The point is: instead of displaying anything, the function takes the result of add and shows an alert whether it is equal to 8.

In a real world application, the next steps would be:

  • Move the functions that call I/O, external services etc. to a separate file
  • Establish a single place where all dependencies to I/O, external services etc. are created

Then, you can switch these dependencies. For testing, or in your production code. And that's a simple way to do dependency injection in Javascript.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .