Search code examples
javascriptchaisinon

Test simple logger functions with full code coverage


I'm using Chai, Sinon and Instanbul to test a NodeJS application. Here's the Logger code:

import Debug, { IDebugger } from 'debug';

export default class Logger {
  private readonly dbg: IDebugger;

  constructor(name: string) {
    this.dbg = Debug(name);
  }

  public log(str: string): void {
    this.dbg(str);
  }
}

Here's the test that I have built to start with:

import * as fs from 'fs';
import sinon from 'sinon';
import { expect } from 'chai';
import Logger from '../../src/utils/Logger';
import Debug from 'debug';

describe('Unit tests for Logger class', () => {
  afterEach(() => {
    sinon.restore();
  });

  describe('#constructor', () => {
    it('should set logger', () => {
      const setLoggerStub = sinon.stub(Logger.prototype, 'log');
      const logExample: string = 'test logger'
      const logger = new Logger(logExample);
      logger.log('test logger')

      sinon.assert.calledWithExactly(setLoggerStub, logExample);
    });
  });
});

The code coverage report is the following: enter image description here

I'm not sure why the log function is not tested, how can I test it and separate the test for the constructor and the test for the log function?

I'm not sure about what to test because the only function of log is to pass a string to the debug library. What should be the approach in this scenario?


Solution

  • log isn't covered because it never got called, you stubbed it out with sinon.stub(Logger.prototype, 'log').

    Your current implementation is difficult to test because Logger is too coupled to Debug. Creating instances of dependencies in the constructor is generally not a good idea.

    If you invert the dependency, making Logger's constructor take an IDebugger instead of the string name:

    import Debug, { IDebugger } from 'debug';
    
    export default class Logger {
    
      constructor(private readonly dbg: IDebugger) {}
    
      public log(str: string): void {
        this.dbg(str);
      }
    }
    

    then when you test Logger, you can easily inject a test double:

    it("debugs the input", () => {
      const debug = sinon.stub();
      const logger = new Logger(debug);
      const message = "hello, world!";
    
      logger.log(message);
    
      sinon.assert.calledWithExactly(debug, message);
    });
    

    This means you don't need to spy on any part of Logger itself, allowing you to refactor inside that class more easily. The Logger class now only depends on the abstract IDebugger interface, not the concrete Debug implementation.

    Creating a Logger instance with a Debug given the specific name can just be a function, or a static method:

    export default class Logger {
    
      static withName(name: string) {
        return new Logger(Debug(name));
      }
    
      /* ... */
    }