Dependency Injection in JavaScript — the Best Tool You’re Not Using for your Tests

Let me introduce you to your new testing best friend

Fernando Doglio
Bits and Pieces
Published in
7 min readOct 27, 2021

--

Photo by Sara Bakhshi on Unsplash

Dependencies in your code can be anything, from a 3rd party library that you’re using to perform a validation to the database where you save all your data.

They’re part of our everyday tasks, but when writing unit tests, we tend to forget that they can’t be part of them. So instead we write tests that depend on them without even realizing it. Why is that bad? Because you’re then having to check false negatives and you’re having to set up quite an infrastructure to get your tests running.

That’s not what unit tests are all about, and in this article I’m going to show you how to fix it.

What’s the problem exactly?

Let’s quickly dive into why I’m saying you’re writing unit tests incorrectly.

While we learn about unit tests, we’re told that they are tests meant to validate the logic around a unit of our code. The definition of “unit” varies from literature to literature, but in essence it refers to the smallest testable portion of your logic. This ensures that you won’t go line-by-line testing, but that you won’t either test full functions, especially not when they take care of multiple things.

We’re then treated with a few examples trying to show you that the concept of “unit” is not fixed. However, hardly ever do they show you a test that deals with writing data to your database. Or perhaps reading a configuration file from disk.

Any I/O operation that you introduce into your test, whether you do it consciousl yor unconsciously is forcing your test to depend on the service you’re interacting with. Yes, I’m referring to the database, or the hard drive, but it can be anything really, an external API for instance.

What happens if you’re running your tests and the disk fails? Your code won’t be able to read that file, but is your logic faulty? Because that’s what the failure of your unit test implies. But it’s not, the service is faulty.

It’s not your unit test’s responsibility to check the stability of an external service, that’s what integration tests are for. You have to make sure your unit tests focus only on your code, and you do that by using dependency injection.

The pattern

The pattern is simple, dependency injection is all about you being able to somehow overwrite the dependencies (also known as services) a piece of code (a.k.a the client) has. So if you’re working with a function that writes to the database, you have to somehow overwrite the DB driver. If you’re dealing with a function calling an external API, you’ll want to overwrite the library that does the HTTP request, and so on.

Now the beauty of this pattern is in how you implement it. If you’re starting from scratch, and even better, if you’re using TDD, the simplest way is to consider this in advance and provide a simple overwrite parameter on every public method and function that you export out of a module. That way you can have a default behavior when not testing, and an “overwrite switch” when writing your tests.

If on the other hand, you’re testing code that was not written thinking about this in advance (which is the most common scenario), you’ll be able to find different ways of achieving this in JavaScript.

Why do this though?

That’s a great question, and I think I danced around the answer to this question in the introduction of this article, but let’s be clear for a second:

If you’re not using dependency injection in your unit tests, you’re doing it wrong.

With that out of the way, let’s see why is that:

  • You do not want to rely on external services that you potentially don’t have any control over, to understand if your logic is stable or not.
  • Without it, you don’t have full control over how these external services will respond, thus adding uncertainty to the behavior of your tests.
  • If these services suffer delays, they will directly affect the performance of your tests. This is not a problem if you have 10 tests, but if you’re working on a big system, that can affect 100’s if not 1000’s of tests. And running tests is usually one of the first steps of any CI/CD pipeline, so you’re also affecting the performance of your deployments.

I’m sure you can come up with a few other potential issues on your own. The point here is that this is all decreasing the stability of your test, which should be 100% all the time. Think about your tests as idempotents, for every execution under the same controlled circumstances, the result should be the same. This is like having a function using a global variable, you can’t really tell if its output will always be the same unless you control that variable.

Here you can’t control external services, thus you’re allowing for side effects in the form of false negatives. That’s why you want to use dependency injection.

How to perform dependency injection in JavaScript?

It’s actually quite simple, thanks to the dynamic nature of this language.

There are many ways to do it, as I already mentioned, and they all depend on your situation.

Best case scenario: you’re building the code while testing it

In these situations, you can simply have something like this:

With that code, we’re declaring the dependencies for our saveData function as the final parameter. Notice I’m using the destructuring syntax to potentially group an overwrite inside a single object. In my code, I’ll always reference q and con no matter where they’ve been defined.

And on a normal execution I can simply call the saveData function with only the first parameter, and the others will default to the imported ones from the database driver package.

However, if I’m testing this function, I can do something like this:

Notice how I overwrote both dependencies. I’m not connecting to the database anymore, and I’m definitely not sending it a query. Instead, I’m forcing the result to always be successful. That way, I can also do this:

I decided that this time the query function would always return false, that way I can safely test the alternate logic path of my function.

This way I don’t need the database to be active and running at any point and my tests will run without delays.

Not ideal: you’re testing code already written and you can’t change it

If on the other hand, you have the task to add tests to a piece of code that’s already been written and for some strange requirement, you can’t modify it to look like the above example, then you have to find more creative ways.

If you’re working in Node.js, for example, you can go with something like proxyquire which allows you to replace the required dependencies inside a file you’re testing without affecting its code whatsoever. For instance, imagine our code looks like this:

There is no easy way to override the dbdriver module from outside, however, with proxyquire, you can do this inside your test:

Now, instead of having a global require for our saveData function, we’re importing it inside our test case, and while we’re at it, we’re also overriding the require call (inside the file) to dbdriver with a custom returned object. We’re not changing the code at all, but this version of saveData will use our stubbed driver, instead of the original one.

If you’re using browsify for instance, there is a version of proxyquire that works with it. You should check it out.

There are other alternatives if you’re into TypeScript such as https://inversify.io/ and https://github.com/typestack/typedi, they’re definitely not as straightforward, but they do provide a very TypeScript-compatible API.

Dependency injection is a very useful tool, one that gets grossly overlooked by many developers, especially when it comes to unit testing. However, it can work wonders in helping you write extensible and reliable code, so give it a try. JavaScript dynamic typing and behavior is ideal to start dipping your toes into the DI waters, so check it out!

What’s your favorite DI library for JavaScript? And most importantly, do you code with DI in mind or work it in while writing the tests of your code?

Build composable web applications

Don’t build web monoliths. Use Bit to create and compose decoupled software components — in your favorite frameworks like React or Node. Build scalable frontends and backends with a powerful and enjoyable dev experience.

Bring your team to Bit Cloud to host and collaborate on components together, and greatly speed up, scale, and standardize development as a team. Start with composable frontends like a Design System or Micro Frontends, or explore the composable backend. Give it a try →

https://cdn-images-1.medium.com/max/800/1*ctBUj-lpq4PZpMcEF-qB7w.gif

Learn More

--

--

I write about technology, freelancing and more. Check out my FREE newsletter if you’re into Software Development: https://fernandodoglio.substack.com/