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);
}
This is what the code prints to the console:
8
99
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);
}
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);
}
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));
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.