Nest: How can you mock external dependencies?

Created on 19 Oct 2017  路  15Comments  路  Source: nestjs/nest

I am trying to test a logger-service, which uses imports from the node_modules winston and fs, and I would like to mock these imports, but I don't know how. With nest it's really easy to mock the injected dependencies, but these dependencies are imported, not injected. For normal Node.js modules I use proxyquire to proxy my required node_modules, but that's not possible with Nest, because the dependencies are imported, not required, and they are imported very early in the process (before the constructor of the service is called).

Here is my logger-service:

import {Component} from '@nestjs/common';
import {Logger, transports} from 'winston';
import * as path from 'path';
import * as fs from 'fs';

import { ConfigService } from '../config/config.service';

require('winston-daily-rotate-file');

@Component()
export class LoggerService {
    logger:any;
    logFolders:any;

    constructor(private config:ConfigService) {
        this.logFolders = {base: path.join(__dirname, '..', '..', '..', 'logs')};
        this.logFolders.debug = path.join(this.logFolders.base, 'debug');
        this.logFolders.exception = path.join(this.logFolders.base, 'debug');

        this._createLogFolders();

        this.logger = new Logger({
            transports: [
                new transports.Console({
                    json: false,
                    timestamp: true,
                    level: config.get('debug') ? 'debug' : 'info'
                }),
                new transports.DailyRotateFile({
                    filename: path.join(this.logFolders.debug, 'debug.log'),
                    json: false,
                    level: 'error',
                    datePattern: 'yyyy-MM-dd-',
                    prepend: true
                })
            ],
            exceptionHandlers: [
                new transports.Console({
                    json: false,
                    timestamp: true,
                    level: config.get('debug') ? 'debug' : 'info'
                }),
                new transports.DailyRotateFile({
                    filename: path.join(this.logFolders.exception, 'execeptions.log'),
                    json: false,
                    level: 'error',
                    datePattern: 'yyyy-MM-dd-',
                    prepend: true
                })
            ],
            exitOnError: false
        });
    }

    private _createLogFolders() {
        const createDir = dir => {
            if (!fs.existsSync(dir)) {
                fs.mkdirSync(dir);
            }
        };

        createDir(this.logFolders.base);
        createDir(this.logFolders.debug);
        createDir(this.logFolders.exception);
    }

    debug(message:string) {
        this.logger.debug(message);
    }

    info(message:string) {
        this.logger.info(message);
    }

    warn(message:string) {
        this.logger.warn(message);
    }

    error(message:string) {
        this.logger.error(message);
    }
}

And this is my test:

import {Test} from '@nestjs/testing';
import {TestingModule} from '@nestjs/testing/testing-module';
import {LoggerService, ConfigService} from '../';
import {expect} from 'chai';
import * as sinon from 'sinon';

describe ('LoggerService', () => {
    let module: TestingModule;
    let service: LoggerService;
    let sandbox;
    let logStub;

    beforeEach(async () => {
        sandbox = sinon.sandbox.create();

        module = await Test.createTestingModule({
            components: [
                LoggerService,
                ConfigService
            ]
        }).compile();

        service = module.get(LoggerService);
    });

    it('should exist', () => {
        expect(service).to.exist;
    });

    it('should log a debug message', () => {
         logStub = sandbox.stub(service.logger, 'debug');
         service.debug('someMessage');
         expect(logStub).to.have.been.calledWith('someMessage');
    });

    it('should log a info message', () => {
        logStub = sandbox.stub(service.logger, 'info');
        service.info('someMessage');
        expect(logStub).to.have.been.calledWith('someMessage');
    });

    it('should log a debug message', () => {
        logStub = sandbox.stub(service.logger, 'warn');
        service.warn('someMessage');
        expect(logStub).to.have.been.calledWith('someMessage');
    });

    it('should log a debug message', () => {
        logStub = sandbox.stub(service.logger, 'error');
        service.error('someMessage');
        expect(logStub).to.have.been.calledWith('someMessage');
    });

});

I am not able to mock winston or fs, so I cannot test the constructor or prevent the creation of the logFolders.

Can anyone help me to mock imports?

question 馃檶

Most helpful comment

You are not following IoC
You must create injectable component from logger package and inject to constructor.

{
  provide: 'Logger',
  useFactory: () => require('winston')
};
constructor(
    private config:ConfigService,
   @Inject('Logger) private logger,
) {}

All 15 comments

Hey !
You could maybe use https://github.com/boblauer/mock-require/blob/master/README.md
Which work like this mock = require('mock-require')
And then mcok('pathDependency', { mockMethod: expectedBehavior });

I already tried mock-require, as well as rewiremock, but the file is loaded at the very beginning of the test, probably because I export all providers in a index.ts, and when a test imports on of those exports, all exports of the index.ts are loaded, including my logger-service, and the mock won't work anymore.

You are not following IoC
You must create injectable component from logger package and inject to constructor.

{
  provide: 'Logger',
  useFactory: () => require('winston')
};
constructor(
    private config:ConfigService,
   @Inject('Logger) private logger,
) {}

@unlight yes you right, sorry i didn't check the details of the code my bad ^^

@unlight thanx! That's exactly what I needed.

Cool :) @unlight great answer!

Currently facing the same problem and came up with the solution like @unlight.
Now the problem I have is, the third-party dependencies do not have any type definitions when using inside of a service, controller, etc.

I tried:

import * as Fs from 'fs';

export class AppController {
  constructor(
   // TSError: Cannot use namespace 'Fs' as a type.
    @Inject('Fs')
    private fs: Fs
  )
}

Anyone found a solution for this?

@kamilmysliwiec also; maybe mention @unlight solution in the NestJS docs FAQ section?

I think fs is part of the @types/node package. For other packages without
type definitions you can just create your own.

Thanks for the quick anwser @mslourens.
I am aware of that. But how do you actually explain TypeScript to use the Fs-Typings of @types/node for my variable?

You cant just use import { fs } from '@types/node' and then add the type to the variable off course.

I hope you understand what I mean.

Ah, I see what you mean. You can still type FS, but probably not the whole module at once. Say if you just want to use the fstat function of the fs module, you have to inject it separately and then type it as fstat, like this:

import { fstat } from 'fs';

export class AppController {
  constructor(
   // TSError: Cannot use namespace 'Fs' as a type.
    @Inject('stat')
    private stat: fstat
  )
}

Your AppModule will look like this:
```js
@Module({
components: [
{ provide: 'stat', useFactory: () => require('fs').fstat },
]
})
export class AppModule {
}
````
The downside of this way of injecting is that you have a lot of dependencies to inject in your controller. That's why I choose to just forget the type.

Thanks for the anwser. That solution would fix my issue, but as you mentioned this is not really convenient. I prefer having no types, rather than creating a wrapper for every function and injecting it, like you do.

Guess there is no better solution at the moment.

@mslourens
You can create new type by using typeof (it behaves differently in TypeScript):

import * as fs from 'fs';

export class AppModule {

    constructor(
       @Inject('fs') private _fs: typeof fs
    ) { }
}

or

import * as fs from 'fs';
type FsType = typeof fs;

export class AppModule {

    constructor(
       @Inject('fs') private fs: FsType
    ) { }
}

Sorry, this reply was for @BrunnerLivio

@unlight
Oh thats awesome, did not know that!
Thanks alot!

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

artaommahe picture artaommahe  路  3Comments

cojack picture cojack  路  3Comments

tronginc picture tronginc  路  3Comments

rlesniak picture rlesniak  路  3Comments

thohoh picture thohoh  路  3Comments