node.jsjestjsmockingts-jestpinojs

How to create a noop logger instance of Pino logger?


I want to test a function (using jest) which takes a pino.Logger type object as an argument. I have failed at trying to mock it, so all I need is a noop Logger instance that does nothing. I'm mainly a Go dev and I don't have any problem defining the noop Zap logger or custom logger that implements Zap logger interface.

I tried mocking it like this:

import Pino from 'pino';

jest.mock('pino');

const childFakeLogger = { warn: jest.fn() };
const fakeLogger = {
  child: jest.fn(() => childFakeLogger)
};

beforeAll(() => {
  Pino.mockImplementation(() => fakeLogger); // err msg="Property 'mockImplementation' does not exist on type 'typeof pino'." 
});

I've tried starting a noop definition like this:

import Pino, { Logger } from 'pino';

const noOp: Pino.LogFn = (..._args: any[]) => {};

export const noopLogger: Logger = {
  child: any, // don't know how to define this
  level: 'trace',
  fatal: noOp,
  error: noOp,
  warn: noOp,
  info: noOp,
  debug: noOp,
  trace: noOp,
};

Which gives the following error:

Type '{ level: string; fatal: Pino.LogFn; error: Pino.LogFn; warn: Pino.LogFn; info: Pino.LogFn; debug: Pino.LogFn; trace: Pino.LogFn; }' is not assignable to type 'Logger'.
Property 'silent' is missing in type '{ level: string; fatal: Pino.LogFn; error: Pino.LogFn; warn: Pino.LogFn; info: Pino.LogFn; debug: Pino.LogFn; trace: Pino.LogFn; }' but required in type 'BaseLogger'.

But I can't figure out how to fully define it according to the Pino.BaseLogger definition.


Solution

  • TL;DR don't make fakeloggers. Either make a real one that is silent or make a stub

    Couple of things:

    1. that error message is complaining about the missing method silent not the child property. silent is a log level and a log type (like debug, info, etc) and coincidently is a noop. so to get past the error you are specifically getting just add it to your noopLogger object:
    import Pino, { Logger } from 'pino';
    
    const noOp: Pino.LogFn = (..._args: any[]) => {};
    
    export const noopLogger: Logger = {
      child: any, // don't know how to define this
      level: 'trace',
      silent: noOp, // this is the complaint
      fatal: noOp,
      error: noOp,
      warn: noOp,
      info: noOp,
      debug: noOp,
      trace: noOp,
    };
    
    1. child is a method that returns a Logger. so good luck making this recursive
    2. creating a real noop pino Logger is annoying because the Logger type looks like this:
    import type { EventEmitter } from 'node:events'
    import type * as pino from 'pino'
    
    export interface FullLogger<
      CustomLevels extends string = never,
      UseOnlyCustomLevels extends boolean = boolean
    > extends EventEmitter {
      // From BaseLogger
      level: pino.LevelWithSilentOrString
      fatal: pino.LogFn
      error: pino.LogFn
      warn: pino.LogFn
      info: pino.LogFn
      debug: pino.LogFn
      trace: pino.LogFn
      silent: pino.LogFn
    
      // From LoggerExtras
      readonly version: string
      levels: pino.LevelMapping
      useLevelLabels: boolean
      levelVal: number
    
      child<ChildCustomLevels extends string = never>(
        bindings: pino.Bindings,
        options?: pino.ChildLoggerOptions<ChildCustomLevels>
      ): pino.Logger<CustomLevels | ChildCustomLevels>
    
      onChild: pino.OnChildCallback<CustomLevels>
    
      on(
        event: 'level-change',
        listener: pino.LevelChangeEventListener<CustomLevels, UseOnlyCustomLevels>
      ): this
      addListener(
        event: 'level-change',
        listener: pino.LevelChangeEventListener<CustomLevels, UseOnlyCustomLevels>
      ): this
      once(
        event: 'level-change',
        listener: pino.LevelChangeEventListener<CustomLevels, UseOnlyCustomLevels>
      ): this
      prependListener(
        event: 'level-change',
        listener: pino.LevelChangeEventListener<CustomLevels, UseOnlyCustomLevels>
      ): this
      prependOnceListener(
        event: 'level-change',
        listener: pino.LevelChangeEventListener<CustomLevels, UseOnlyCustomLevels>
      ): this
      removeListener(
        event: 'level-change',
        listener: pino.LevelChangeEventListener<CustomLevels, UseOnlyCustomLevels>
      ): this
    
      isLevelEnabled(level: pino.LevelWithSilentOrString): boolean
      bindings(): pino.Bindings
      setBindings(bindings: pino.Bindings): void
      flush(cb?: (err?: Error) => void): void
    }
    

    so what i do is just avoid it :-P. Instead I propose you do this:

    If you want to disable logging during tests, configure your logger like this:

    import pino from 'pino'
    
    const logger = pino({
      level: process.env.NODE_ENV === 'test' ? 'silent' : 'info',
    })
    

    now there is no need to mock them to shut them up.

    You should really only be mocking the logger (like you were with your noopLogger) when:

    1. you need to spy on calls in unit tests like when you do vi.spyOn() or jest.fn()
    2. you are injecting a logger into many parts of your app and want a drop-in fake for test isolation

    In those cases, what you want to do is create a stub like this:

    import { vi } from 'vitest'
    import type { Logger } from 'pino'
    
    const logger = {
      // just define the properties and methods that get called
      info: () => {},
      error: () => {},
    } as Logger
    
    vi.spyOn(logger, 'info')
    
    myFunctionThatLogs(logger)
    
    expect(logger.info).toHaveBeenCalledWith('expected message')
    

    LMK if you have further questions or if i was unclear on anything