Search code examples
unit-testingjestjslog4js-node

How to mock a call to logger.warn?


I'm practicing test-first development and I want to ensure that method in a class always calls my logger at the warn level with a message. My class is defined like so:

import { log4js } from '../config/log4js-config'

export const logger = log4js.getLogger('myClass')

class MyClass {
  sum(numbers) {
    const reducer = (accumulator, currentValue) => accumulator + currentValue
    const retval = numbers.reduce(reducer))
    if (retval < 0) {
      logger.warn('The sum is less than zero!')
    }
    return retval
  }
}

const myClass = new MyClass()
export { myClass }

My test looks like this:

import { myClass, logger } from './MyClass'
import { log4js } from '../config/log4js-config'

jest.mock('log4js')

describe('MyClass', () => {

  it('logs a warn-level message if sum is negative', () => {
    logger.warn = jest.fn()
    logger._log = jest.fn()
    myClass.sum([0, -1])
    expect(logger.warn).toHaveBeenCalled() // <--- fails
    expect(logger._log).toHaveBeenCalled() // <--- fails
  })
})

I've also tried to mock log4js.Logger._log in the setup but that didn't seem to work either. 😕 Any suggestions are appreciated!


Solution

  • The thing with mocking is that you need to provide the mock, simplest method for me is through the mock factory. However i would recomend also some refactoring:

    import { getLogger } from 'log4js'
    
    export const logger = getLogger('myClass')
    logger.level = 'debug'
    
    // export the class itself to avoid memory leaks
    export class MyClass {
      // would consider even export just the sum function
      sum(numbers) {
        const reducer = (accumulator, currentValue) => accumulator + currentValue
        const retval = numbers.reduce(reducer))
        if (retval < 0) {
          logger.warn('The sum is less than zero!')
        }
        return retval
      }
    }
    
    
    import log4js from 'log4js';
    import { MyClass } from "./class";
    
    jest.mock('log4js', () => {
        // using the mock factory we mimic the library.
    
        // this mock function is outside the mockImplementation 
        // because we want to check the same mock in every test,
        // not create a new one mock every log4js.getLogger()
        const warn = jest.fn()
        return {
            getLogger: jest.fn().mockImplementation(() => ({
                level: jest.fn(),
                warn,
            })),
        }
    })
    
    beforeEach(() => {
        // reset modules to avoid leaky scenarios
        jest.resetModules()
    })
    
    // this is just some good habits, if we rename the module
    describe(MyClass, () => {
    
        it('logs a warn-level message if sum is negative', () => {
            const myClass = new MyClass()
            myClass.sum([0, -1])
    
            // now we can check the mocks
            expect(log4js.getLogger).toHaveBeenCalledTimes(1) // <--- passes
            // check exactly the number of calls to be extra sure
            expect(log4js.getLogger().warn).toHaveBeenCalledTimes(1) // <--- passes
        })
    
    })