Boost Testability with Dependency Injection in JavaScript: Practical Examples

Vijay Rangan
5 min readJun 2, 2023

--

You’ve likely read that dependency injection (or DI) is this great tool that helps write better code. If you’re like me, having heard or read about this so… many… times but never seen or understood how it actually works, you’re in for a treat. Today, we’re going to look at some practical examples of how DI actually helps.

Welcome back, coding enthusiasts! Today we turn our attention to a crucial aspect of the use of DI: testability.

Testability is all about making our code easy to test

To illustrate this, we’ll revisit Jane’s Coffee Shop. So, grab your favourite cuppa joe ☕️ and let’s dive into the world of testability and dependency injection!

Testing…

“Testing, testing, testing” — we hear this so much. What does it even mean?

As programmers, we’re testing all the time — running a command on the terminal, testing an API with a tool like Postman etc… The only difference is that most of us are testing code manually. When I say testing, I mean automated testing.

By writing automated tests, we can verify the correctness of our code, catch bugs early on and ensure that future modifications don’t break existing functionality, in a way that is repeatable.

Testability, on the other hand, is about writing code that is easy to test. Dependency injection plays a pivotal role in achieving this.

Quick Setup

I personally like using Jest and Sinon to write tests. Jest is a testing framework that’s easy to setup and work with, and so is Sinon.

If you aren’t familiar with these tools, don’t worry. You should be able to follow along either way.

Let’s add these tools to our project.

yarn add -D jest sinon @types/jest

Shameless plug: If you want to learn about how Sinon works, check out my YouTube video or this article

Once that’s done, we’ll update the package.json file so we can easily run our tests from the CLI using npm run test , like so

{
"name": "coffee-shop",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"test": "jest"
},
"devDependencies": {
"@types/jest": "^29.5.2",
"jest": "^29.5.0"
}
}

Finally, we’ll add a test directory. Jest, by default, looks for test files in this directory and any file ending with .spec.js or .test.js

The project structure for testing

Your first test

We’ll write tests for Jane’s Barista class.

If you read the prequel to this post, you’ll remember that we decoupled the Baristaclass from the CoffeeGuruclass using DI. This has allowed us now to create a fake implementation of it’s dependencies and test it in isolation, ie, only focus on the Barista class and not worry about anything else.

In testing, we follow a structure called Arrange Act Assert . It simply means for each test, we

  1. Arrange — the world by creating instances, setting up seed data etc…
  2. Act — call the function or class that we are testing. This is also called System Under Test (or SUT)
  3. Assert — or make sure that the result from calling the SUT is what you expect it to be.

Running the above with npm run test produces the following

Alright, alright. This is all fine and dandy but where is this “power of DI” that you speak of? I love your enthusiasm! Let’s dig in.

Flexing the code

One of the advantages of dependency injection is the ability to put our code through various scenarios. Let’s explore a test case where the Coffee Guru fails to provide coffee beans, forcing the Barista to handle the situation gracefully.

As you can see, we modified CoffeeGuru to return an unexpected value. In the current implementation, this test fails.

This is an opportunity to go update our implementation to handle this situation. Let’s update our Barista class to handle this case.

Finally, we’ll test another scenario where we simulate an error during the milk retrieval process. Once again, we update our test first to setup expectations from our code and see that our test fails.

Now that we know this can happen, we’ll update our code to handle this by wrapping it in a try...catch block.

There! Our tests are back to green.

If you didn’t notice, there’s a pattern here. Each of our examples above took the following shape:

  1. Write a test scenario
  2. Run the test and see it fail
  3. Update the code to handle the scenario
  4. Re-run the test and see it pass.

This cycle is known as the Red-Green-Refactor cycle and is the mantra of Test Driven Development.

Conclusion

By decoupling dependencies and replacing them with mocks or stubs (collectively called fakes), we can isolate and thoroughly test our code. The ability to simulate different scenarios and gracefully handle errors ensures the reliability and robustness of our applications. So, harness the potential of dependency injection and take your testing game to the next level!

I hope you’re now able to see how DI helps in the real world and got a good understanding of how to use it in the real world.

I’d love to know what you think about these articles!
Are they useful? What are you looking to learn more about? React, Node / Express / databases and ORMs?

I have been a full stack engineer for about 9 years and have a wide array of experience from web development with React and Vue to architecting micro services to setting up and automating infrastructure.

As always, if you liked this article and you learned something new, show some love with a 👏 . It really helps reach a wider audience and motivates me to write more. 🙌🏻

Until next time…

--

--