node.jsunit-testingjestjsmockingyargs

Writing unit test case for third party cli package


I have a basic CLI program built using yargs. I'm able to cover test cases for exported functions in the application.

As you can see below test coverage is not done from line 12-18 which. How do we write unit test coverage for third-party package like yargs?

index.js

const yargs = require('yargs');
const { hideBin } = require('yargs/helpers');
const greet = (name) => {
  return `Welcome ${name}`;
};
yargs(hideBin(process.argv)).command(
  'run [name]',
  'print name',
  (yargs) => {
    yargs.positional('name', { describe: 'Your name', type: 'string' });
  },
  (args) => {
    const { name } = args;

    const greetMsg = greet(name);

    console.log(greetMsg);
  }
).argv;

module.exports = { greet };

index.test.js

const { greet } = require('./index')

describe.only('greeting', () => {
  it('greet', async () => {
    const greetMsg = greet('test')

    expect(greetMsg).toBe('Welcome test')
  })
})

test coverage

PASS  ./index.test.js
greeting
  ✓ greet (5 ms)

----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |   63.64 |      100 |   33.33 |   63.64 |                   
index.js  |   63.64 |      100 |   33.33 |   63.64 | 12-18            
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.316 s, estimated 2 s
Ran all test suites.

Solution

  • You can use jest.doMock(moduleName, factory, options) to mock yargs and yargs/helpers module. Since the yargs function in the module scope will be executed when requiring the module. You need to use jest.resetModules() to resets the module registry - the cache of all required modules for each test case before requiring the index.js module.

    This is useful to isolate modules where local state might conflict between tests.

    E.g.

    index.js:

    const yargs = require('yargs');
    const { hideBin } = require('yargs/helpers');
    
    const greet = (name) => {
      return `Welcome ${name}`;
    };
    yargs(hideBin(process.argv)).command(
      'run [name]',
      'print name',
      (yargs) => {
        yargs.positional('name', { describe: 'Your name', type: 'string' });
      },
      (args) => {
        const { name } = args;
    
        const greetMsg = greet(name);
    
        console.log(greetMsg);
      }
    ).argv;
    
    module.exports = { greet };
    

    index.test.js:

    describe.only('greeting', () => {
      beforeEach(() => {
        jest.resetModules();
      });
      it('greet', () => {
        const { greet } = require('./index');
        const greetMsg = greet('test');
        expect(greetMsg).toBe('Welcome test');
      });
    
      it('should pass', () => {
        jest.doMock('yargs');
        jest.doMock('yargs/helpers');
        const yargs = require('yargs');
        const { hideBin } = require('yargs/helpers');
        const mArgv = {
          command: jest.fn().mockImplementation(function (command, description, builder, handler) {
            builder(this);
            handler({ name: 'teresa teng' });
            return this;
          }),
          argv: {},
          positional: jest.fn(),
        };
        yargs.mockReturnValueOnce(mArgv);
        require('./');
        expect(hideBin).toBeCalled();
        expect(yargs).toBeCalled();
        expect(mArgv.positional).toBeCalledWith('name', { describe: 'Your name', type: 'string' });
      });
    });
    

    test result:

     PASS  examples/67830954/index.test.js (7.17 s)
      greeting
        ✓ greet (355 ms)
        ✓ should pass (23 ms)
    
      console.log
        Welcome teresa teng
    
          at examples/67830954/index.js:18:13
    
    ----------|---------|----------|---------|---------|-------------------
    File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
    ----------|---------|----------|---------|---------|-------------------
    All files |     100 |      100 |     100 |     100 |                   
     index.js |     100 |      100 |     100 |     100 |                   
    ----------|---------|----------|---------|---------|-------------------
    Test Suites: 1 passed, 1 total
    Tests:       2 passed, 2 total
    Snapshots:   0 total
    Time:        7.668 s, estimated 8 s