Why do I have to use Dependency Injection in JS?

Why do I have to use Dependency Injection in JS?

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
    };
  }
};

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
    };
  }
};

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.');
    });
  });
});

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]);

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.