4

I have a controller that uses NestJS built-in Logger via dependency injection in constructor of the controller:

  constructor(private readonly logger: Logger) 

I want to be able to mock it in my Jest tests to see which methods and with what arguments are being called during logging. I tried this syntax:

providers[{
    provide: Logger,
    useValue: {
      log: jest.fn(),
    }
}]

In that case this line:

    expect(Logger).toHaveBeenCalledTimes(1);

Returns: Matcher error: received value must be a mock or spy function

Any help will be highly appreciated!

Dave Anders
  • 799
  • 6
  • 13

3 Answers3

13

In your test, you should get the logger back out of the DI context using moduleFixture.get(Logger) (or something very similar) and then check expect(logger.log).toHaveBeenCalledTimes(1). Logger itself is a class, not a spy or mock, so Jest doesn't know what to do with that.

Full solution that worked:

import { Test } from '@nestjs/testing';
let logger: Logger;

beforeEach(async () => {
  const moduleRef = await Test.createTestingModule({
    providers: [  
      {
        provide: Logger,
        useValue: {
          log: jest.fn(),
        },
      },
    ],
  }).compile();
  logger = moduleRef.get<Logger>(Logger);
});

And then later in the test itself:

expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('Your log message here')
Dave Anders
  • 799
  • 6
  • 13
Jay McDoniel
  • 57,339
  • 7
  • 135
  • 147
  • Indeed, I was trying so many options that got confused in them. What worked is: logger = moduleRef.get(Logger); And then: expect(logger.log).toHaveBeenCalledTimes(1); – Dave Anders Jul 16 '21 at 06:22
  • I still couldn't get it to work for whatever reason, so I ended up creating a new logger class that extends `ConsoleLogger` and dependency injected it in since that was a standard for testing anyways. It ended up working nicely because I could put Sentry in that extended class abstracted out of other locations which made testing easier down the road . – CTS_AE Dec 07 '22 at 00:51
  • It's frustrating that their documentation is lacking around how to use the built-in logger. I end up back here a few months later looking again I'm not sure why their logger doesn't seem to follow their own dependency injection model which would make this like testing anything else in the NestJS DI ecosystem. – CTS_AE Mar 29 '23 at 21:00
  • @CTS_AE most likely it depends on how the logger is used. I use a custom logger I wrote called [ogma](https://ogma.jaymcdoniel.dev) most of the time. – Jay McDoniel Mar 29 '23 at 22:04
0

I'm going to leave this as an answer for how I've been handling this lately. So next time I'm Googling and end up here I'll remember

jest.mock Without Success

I've tried using jest.mock like I normally would on imports for the Logger class from @nest/common, but it seems to cause its own problems. Even if you try to keep the original implementation like so:

jest.mock('@nestjs/common', () => ({
  ...jest.requireActual('@nestjs/common'),
  Logger: jest.fn(),
}))

I still want to believe there has to be a way to accomplish it like this, but maybe Nest JS's dependency system circumvents Jest's hoisted mocking?

✅ Using a Custom Logger with Nest JS's Dependency Injection

This feels like unnecessary lifting, but it follows Nest JS's dependency injection, and allows for extending or overwriting later. If you're already writing tests for Nest JS you're likely familiar with it already.

custom.logger.ts

import { ConsoleLogger } from '@nestjs/common'

export class CustomLogger extends ConsoleLogger {}

some-consumer.spec.ts

This approach uses the jest-mock-extended library, but you could also do something like @Jay McDoniel's answer as well.

import { SomeConsumer } from './some-consumer'
import { CustomLogger } from './custom.logger'
import { Test } from '@nestjs/testing'
import { mockDeep } from 'jest-mock-extended'

describe('SomeConsumer', () => {
  let someConsumer: SomeConsumer
  const logger = mockDeep<CustomLogger>()

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        SomeConsumer,
        {
          provide: CustomLogger,
          useValue: logger,
        },
      ],
    }).compile()

    someConsumer = module.get(SomeConsumer)
  })

  it('should do something', () => {
    const result = someConsumer.doSomething()

    expect(result).toEqual('something returned')
  })

  it('should log something', () => {
    someConsumer.doSomething()

    expect(logger.log).toHaveBeenCalledWith('something')
  })
})

some-consumer.ts

I figured I would provide an example of the logger being consumed.

import { Injectable } from '@nestjs/common'
import { CustomLogger } from './custom-logger'

@Injectable()
export class SomeConsumer {
  constructor(private readonly logger: CustomLogger) {}

  public doSomething(): string {
    this.logger.log('something')

    return 'something returned'
  }
}

✅ A Second Try with the imported Logger from @nestjs/common

I saw elsewhere someone mentioning you could set the logger in the module, so I gave it a shot and it seems to work as well

some-consumer-imported.ts

import { Injectable, Logger } from '@nestjs/common'

@Injectable()
export class SomeConsumerImported {
  private logger = new Logger(SomeConsumerImported.name)

  public doSomething(): string {
    this.logger.log('something logged')

    return 'something returned'
  }
}

some-consumer-imported.spec.ts

import { SomeConsumerImported } from './some-consumer-imported'
import { Logger } from '@nestjs/common'
import { Test } from '@nestjs/testing'
import { mockDeep } from 'jest-mock-extended'

describe('SomeConsumerImported', () => {
  let someConsumerImported: SomeConsumerImported
  const logger = mockDeep<Logger>()

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [SomeConsumerImported],
    }).compile()

    module.useLogger(logger)

    someConsumerImported = module.get(SomeConsumerImported)
  })

  it('should do something', () => {
    const result = someConsumerImported.doSomething()

    expect(result).toEqual('something returned')
  })

  it('should log something', () => {
    someConsumerImported.doSomething()

    expect(logger.log).toHaveBeenCalledWith('something logged', SomeConsumerImported.name)
  })
})

CTS_AE
  • 12,987
  • 8
  • 62
  • 63
-1

You can just use jest.SpyOn on the Logger class directly

 jest.spyOn(Logger, 'log');

 expect(Logger.Log).toHaveBeenCalledTimes('error', 'SmsService.sendSMS');

Bitneko
  • 123
  • 1
  • 1
  • 7