DEV Community

Cover image for Why do I have to use Dependency Injection in JS?
Vyzaldy Andrés Sanchez
Vyzaldy Andrés Sanchez

Posted on

Why do I have to use Dependency Injection in JS?

Dependency Injection

Anytime we go into a project, existent or newly created, we always think about how what we'll build can be more manageable, scalable and and easy to test. This is where using Dependency Injection can come in handy for us.

But first, what do we mean by Dependency Injection?

It is a software design pattern that allow us to make code unit testable, by moving the responsibility for getting dependencies outside the code that depends on it. It also refers to the action of supplying some dependencies, from some part in our code, to some object, function or module that depends on those dependencies.

Why is this useful?

As said earlier, we can make pieces of our code way easy to test if we abstract them of knowing the specific dependencies they need, when needed. For example:

//File: services/notifications/index.js

import User from '../database/models/user';
import { logError } from './logger';
import { sendEmailNotification } from './emails';

const DEFAULT_NOTIFICATION_MESSAGE = 'Hi, friend. :)';

export const sendNotificationsToUsers = async (ids = []) => {
  try {
    const users = await User.find({
      id: ids
    });

    const promises = users.map(({
      email,
      // This we'll add notifications into a queue to process them in the background.
      // Don't freak out.
    }) => sendEmailNotification(email, DEFAULT_NOTIFICATION_MESSAGE));

    await Promise.all(promises);

    return {
      success: true
    };
  } catch (e) {
    logError(e);

    return {
      success: false
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

In the previous example we are trying to send notifications to some users. Nothing strange here. But what do we have to do in order to test this? Is it easy to mock this 3 dependencies in order to test this as a unit?

For me, no.

What would I do?

We can have two cases here going on. The first one, if only this function in our module needs the dependencies. Or the second one, that all functions in our module needs these dependencies.

For the first case:

//File: services/notifications/index.js

const DEFAULT_NOTIFICATION_MESSAGE = 'Hi, friend. :)';

export const sendNotificationsToUsers = async ({
  User,
  logger,
  notifier
}, ids = []) => {
  try {
    const users = await User.find({
      id: ids
    });

    const promises = users.map((user => notifier.notifyUser(user, DEFAULT_NOTIFICATION_MESSAGE)));

    await Promise.all(promises);

    return {
      success: true
    };
  } catch (e) {
    logger.logError(e);

    return {
      success: false
    };
  }
};
Enter fullscreen mode Exit fullscreen mode

What we did here was a bit of refactoring:

  • We pass the dependencies as the first configuration parameter in our sendNotificationsToUsers function.
  • We allow our function to not care about what kind of logger or notifier we need so this function can be generic and can be reused in the future. Like using a SMS notification or whatever comes to our mind.

Now this piece of code is testable and dependencies can be mocked:

//some test file
import assert from 'assert';
import {
  sendNotificationsToUsers
}
from '../core/services/notifications';

describe('Notification service', () => {
  const mockUserDB = {
    find() {
      return Promise.resolve([{
        email: 'some-email@gmail.com',
        phone: 'some-phone-number'
      }]);
    }
  };
  const logger = {
    logError(e) {
      console.log(e);
    }
  }

  describe('#sendNotificationsToUsers', () => {
    it('can send notifications via emails', async () => {
      const notifier = {
        notifyUser(_user, _message) {
          return Promise.resolve(true);
        }
      };
      const notificationResponse = await sendNotificationsToUsers({
        User: mockUserDB,
        logger,
        notifier,
      }, [1]);

      assert(notificationResponse, 'Notifications failed to be sent.');
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

What about the whole module asking for dependencies?

We'll just have to export our module as a function that accepts these dependencies and use it as follows:

export default ({
  User,
  logger,
  notifier
}) => ({
  async sendNotificationsToUsers(ids = []) {
    try {
      const users = await User.find({
        id: ids
      });

      const promises = users.map((user => notifier.notifyUser(user, DEFAULT_NOTIFICATION_MESSAGE)));

      await Promise.all(promises);

      return {
        success: true
      };
    } catch (e) {
      logger.logError(e);

      return {
        success: false
      };
    }
  }
});

//Usage

import User from 'services/users';
import logger from 'services/logger';
import notifier from 'services/emails';
import getNotificationsService from 'services/notifications';

const { sendNotificationsToUsers } = getNotificationsService({ User, logger, notifier });

sendNotificationsToUsers([1, 2, 3]);
Enter fullscreen mode Exit fullscreen mode

Conclusion

I believe that this way of coding will be helpful for all of us, it will help us to write our modules as true units and it will also help us to be more productive while testing and developing.

Please share your thoughts, corrections or comments below and until the next time. Happy Coding.

Top comments (7)

Collapse
 
ld00d profile image
Brian Lampe

Did you know that Jest can mock your modules?

jestjs.io/docs/en/mock-functions.h...

Collapse
 
vyzaldysanchez profile image
Vyzaldy Andrés Sanchez

Hi Brian,

Yes, I do know Jest can achieve that but in my eyes that's kind of bit of magic... Having to import the direct modules installed via npm in the tests files and then override them with with another lib just doesn't feel right to me...

What about when I change that lib for some reason? I'm required to go to that file and change that by having lot of tests broken at that point, and I'll also have to change every module file in order to reflect the replacement which can be avoided using this approach. I prefer abstracting that responsibility of my modules and mocked them myself.

Hope this clarifies my point of view to you. Also, thank you for bringing the Jest fact here, so other guys knows there are already alternatives for this if they didn't know before.

Collapse
 
imthedeveloper profile image
ImTheDeveloper

Agree on the mocking. It feels dangerous to be faking npm modules to try and gain control of your test. Much simpler with injection and you aren't getting tied up in the mocking/unmocking workflows

Thread Thread
 
ld00d profile image
Brian Lampe

Isn't that the whole point though? You inject the npm modules with the sole purpose of mocking them. So in your unit test, you're not testing axios' connection to the web service, but via mocking, you can intercept those calls and stub a response for your unit test.

Collapse
 
vyzaldysanchez profile image
Vyzaldy Andrés Sanchez

Hi Nick, thank you for taking your time and reading this post.

I believe we as developers need to make conscience about how important is making our code as testable, reusable and simple as we can.

We are not the only ones that are going to work in the codebase, we need to make things easier for future maintainers, and companies so what we've built can prevent horrible errors along the road.

Collapse
 
sebbdk profile image
Sebastian Vargr • Edited

If the dependencies are updated and the API changed, then our test will not' fail because the mocks did not get updated.

Which is why integration tests should always be done with real dependencies.

On top of that dependency injection is a anti-pattern on the level of inflection and convention over configuration.

It's hard to debug, hard to test, and hard to maintain or requires a lot of domain knowledge.

We have functional programming patterns today, which can serve the same purposes but without the drawbacks listed above.


TL;DR
Use arguments, not dependency injection.

Dependency injection is how you get AngularJS (and Angular for mysterious reasons).

Edit, Mis-worded a sentence.

Collapse
 
anortef profile image
Adrián Norte

I like that but I find poor man's dependency injection to be more explicit while achieving the same result.