bulletproof-nodejs icon indicating copy to clipboard operation
bulletproof-nodejs copied to clipboard

Dependency Injection in tests

Open Shisuki opened this issue 4 years ago • 2 comments

Hey @santiq

First of all thank you so much for your articles and boilerplate code, you're a true inspiration and a great teacher.

Sorry to bother you if you're busy right now, I'm having some trouble with the dependency injection while testing the authentication process.

I get an error telling me the logger/userModel/eventDispatcher instances injected in the AuthenticationService constructor are undefined, the call stack traces back my users.ts route.

That just seems to happen If I instantiate the AuthenticationService in my test before calling the route, instantiation can either be by TypeDi container or by calling the AuthenticationService directly.


Here's the route I'm testing:

users.ts

import { Router, Request, Response, NextFunction } from 'express';
import { celebrate, Segments } from 'celebrate';
import User, { userJoiSchema } from '../../models/user';
import { IUserDTO } from '../../interfaces';
import { Container } from 'typedi';
import AuthenticationService from './../../services/authentication';

const route = Router();

export default (app: Router) => {
  app.use('/users', route);

  route.post(
    '/',
    celebrate({
      [Segments.BODY]: userJoiSchema.registration,
    }),

    async (req: Request, res: Response, next: NextFunction) => {
      const logger = Container.get<Utils.Logger>('logger');

      try {
        const userDTO: IUserDTO = req.body;

        const userInDB = await User.findOne({
          email: userDTO.email,
        });

        if (userInDB) return next({ status: 200, message: 'User already exists' });

        const authenticationService = Container.get(AuthenticationService);
        const { user, token } = await authenticationService.Register(userDTO);

        res.status(201).json({
          user,
          token,
        });
      } catch (error) {
        logger.error('🔥 error: %o', error);
        return next(error);
      }
    },
  );
};

As you can see nothing is dramatically different from your own auth route. The authenticationService is also the same, I just changed some method names for personal clarity.


Here's the actual test trimmed down to isolate what's wrong

auth.test.ts

import request from 'supertest';
import http from 'http';
import User from '../../models/user';
import factory from '../../Utils/factory';

describe('Auth Test', () => {
  let server: http.Server;

  beforeAll(async (done) => {
    server = await require('../../app').default; //Exporting the server promise from app.ts

    done();
  });

  afterAll(async (done) => {
    server.close();
    await User.deleteMany({});

    done();
  });

  it('should return 201 if request is valid', async (done) => {
    const userDTO = factory.userDTO.build();
    const res = await request(server).post('/users').send(userDTO);

    expect(res.status).toBe(201);

    done();
  });
});

So this test passes without any problems: image


And here's the same test but with a simple instantiation of the AuthenticationService:

auth.test.ts

import request from 'supertest';
import http from 'http';
import User from '../../models/user';
import factory from '../../Utils/factory';
import Container from 'typedi';
import AuthenticationService from '../../services/authentication';
import LoggerInstance from '../../loaders/logger';
import { mock } from 'jest-mock-extended';
import { EventDispatcherInterface } from '../../interfaces';

describe('Auth Test', () => {
  let server: http.Server;

  beforeAll(async (done) => {
    server = await require('../../app').default;

    done();
  });

  afterAll(async (done) => {
    server.close();
    await User.deleteMany({});

    done();
  });

  it('should return 201 if request is valid', async (done) => {
    const authenticationService = new AuthenticationService(
      User,
      LoggerInstance,
      mock<EventDispatcherInterface>(),
    );

    // You can also instantiate it by container, it returns the same error
    // const authenticationService = Container.get(AuthenticationService);

    const userDTO = factory.userDTO.build();
    const res = await request(server).post('/users').send(userDTO);

    expect(res.status).toBe(201);

    done();
  });
});

Here's the error:

crep-error


So I'm hoping to get some clarity in this because some of my other tests depend on the authService to register a user before using supertest to call the route.

Thank you for your time.

Shisuki avatar May 27 '20 14:05 Shisuki

Hi @Shisuki sorry for taking so long in responding.

This is a perfect example of the way of doing unit testing using dependency injections (asked here #65) thanks for taking the time to write a few snippets of code to showcase it.

For what I see the problem is with the Logger you are passing to the authentication service instance, did you instantiated it first? or are you just passing the class?

Since this the authentication service was written with the dependency injection pattern, you can pass a dummy object that respects the interface of Logger but without the implementation of the methods

 it('should return 201 if request is valid', async (done) => {
    const aMockedLoggerInstance = {
      info() {
           // nothing!
      }
   }

    const authenticationService = new AuthenticationService(
      User,
      aMockedLoggerInstance,
      mock<EventDispatcherInterface>(),
    );

    // You can also instantiate it by container, it returns the same error
    // const authenticationService = Container.get(AuthenticationService);

    const userDTO = factory.userDTO.build();
    const res = await request(server).post('/users').send(userDTO);

    expect(res.status).toBe(201);

    done();
  });

This can be pretty useful when you test services that depend on other and you don't want to execute the code but rather just make sure it is called.

santiq avatar Jun 02 '20 21:06 santiq

Hey @santiq thanks for your fast reply and I'm so sorry for taking so long to come back to you..

If you see in my example above it wasn't because I didn't use a dummy object for as a logger, that works fine, but when it works on the tests the implementation on the actual route breaks, and even in the test if I use the DI-container twice it also breaks (All shown above)

Anyway I've given up on using any DI-Container months ago as it ended up making my life more difficult and in the long term everything becomes tightly coupled with the Container.

Right now I've turned to pure DI with the use of index files as interfaces for the services and then barrel files to export exactly what I need, which gives me the best of both worlds and with just moderately more code configuration.

Thank you for your time!

Shisuki avatar Aug 02 '20 17:08 Shisuki