DEV Community

Stuart Dotson
Stuart Dotson

Posted on • Originally published at stuartdotson.com

4 things I learned writing tests for my CLI

I recently updated tests in my open source library get-open-prs. My goal was to get as close as I possibly could to 100% test coverage. However, there were a few obstacles I had to overcome to do so. get-open-prs is a CLI and has a lot of side effects like printing to the console or waiting for user input.

Stub out console.log

let consoleStub;  

beforeEach(() => {
    consoleStub = sinon.stub(console, 'log');  
});

afterEach(() => {
    consoleStub.restore();
})

// ...then later after executing function with console.log side effect.
assert(consoleStub.calledWith('there'), 'console.log should be called with argument 2');
Enter fullscreen mode Exit fullscreen mode

Stub out a 3rd party module or your own

In my case, I was using Inquirer to print cool prompts to the console to get input from the user. Well, similar to how I did console.log\ in the previous tip.

const inquirer = require('inquirer’);
const inquirerStub = sinon.stub(inquirer, 'prompt');

inquirerStub.withArgs(question).resolves({
  prToOpen: 'pr-url-1'
});

assert(inquirerStub.calledWith(question), 'inquirer.prompt for pr question should be called');
Enter fullscreen mode Exit fullscreen mode

I also implemented a dependency-injection pattern to build the main function orchestrating the various dependencies of printing output, getting prs, and updating configuration values. This makes testing this function a whole lot easier and more precise as I just test the business logic and not “how it is done”. The “how” is tested in other unit tests. You can see what I did here: https://github.com/sdotson/get-open-prs/blob/master/src/getOpenPrs.js

Vary how functions respond on successive calls

Sometimes functions were called multiple times with the same args, but with different results. Did I write crappy non-deterministic code? Not exactly but I did involve a source of entropy in the program: human input. Inquirer prints prompts to the console that is then responded to by our human entropy source. Sinon has this great chaining syntax to describe this scenario:

const inquirerStub = sinon.stub(inquirer, 'prompt');      
inquirerStub.withArgs(question)        
  .onFirstCall().resolves({          
    githubToken: ''        
  })        
  .onSecondCall().resolves({          
    githubToken: 'TOKEN'        
  });
Enter fullscreen mode Exit fullscreen mode

The only downside is that that in the assertion part of the test, you can’t chain callCount to the result of calledWith(). So in the case of Inquirer.prompt(), which is essentially called for every single question, you’ll have to count all invocations instead of just the easier-to-read count for a given set of arguments. Something like:

assert(inquirerStub.callCount === 4, 'inquirer should be called twice for continue and twice for prs');
Enter fullscreen mode Exit fullscreen mode

reset() and restore()

beforeEach(() => {
  sinon.reset();  
});

afterEach(() => {    
  inquirerStub.restore();  
});
Enter fullscreen mode Exit fullscreen mode

One important function to remember is sinon.reset(), which resets both the behavior and history of all stubs. If you just want to reset a specific stub you can use stub.reset().

Another is stub.restore(), which is used to restore the original functionality to the stubbed function. You want your tests to be independent of each other and adding this to stubbed methods will help guarantee that.

If you’d like to learn more about sinon, take a look at the official documentation: https://sinonjs.org/releases/latest/stubs/

Top comments (0)