Easy TDD Setup for Nodejs ES6 Mocha Chai Istanbul

Datetime:2016-08-22 21:35:31          Topic: Node.js  ECMAScript           Share

Introduction

I’m working on a command line tool for AngularJS, Angular2, and Aurelia that creates components from user templates.  It creates the folder, component js file, component template HTML file, optional component template CSS file, and the component spec js file.

The tool generates the code using the underscorejs template engine.  It’s amazing how much code you’ll no longer have to type; boiler maker component wiring up and default unit tests for most components.

As I was writing the tool, I decided to break out the project setup into this small blog post to make the tool blog post simpler and focused. You can use this simple project as a starter or learning tool for your Nodejs ES6 projects.

I wrote this application and the command line tool using the Atom Editor.  I’ve include my Atom Snippets down below that give me a big productivity boost when writing unit tests.

This blog post is much more about setting up a Nodejs project that uses ES6, Mocha, Chai, and Istanbul than how to use these tools. Please refer to the many outstanding blog posts, courses, and tutorials on these tools and ES6.

My Approach To Nodejs ES6

It’s amazing what you can write using Nodejs.  I’ve written complex, multi-process apps that have IoT connected over MQTT and real-time communication to web clients.  Also written simple apps like the above command line tool. Nodejs is wonderful and is what enables Electron to be the prodigious cross-platform desktop application tool that it is.

ES6 is a clean modern language, is simple, familiar looking, and is fun.  I’ve used ES5 and TypeScript  for many projects but settled on ES6. I blogged about my decisionhere. 

Using ES6 with Nodejs does not require Babel for your code or unit tests.  I’m not using ES7 features such as class properties or decorators, but I can live with that for now.

I structure my Nodejs apps, perhaps differently than you’ve seen on other blog posts.  Not implying better, just different.

I prefer to write my ES6, Nodejs code like I would any object orientated app, small classes with discrete functionality. In architecture speak, SOLID, DRY, etc.

I also structure my ES6 so that it can be tested.  Sometimes that requires a little rethinking and possibly some refactoring, but it’s worth it.

Hello World

It would be madness to not write the ubiquitous “Hello World” app for my Nodejs demo, so here we go.

When this app is executed, index.js is the entry point, it creates an instance of HelloWorld and invokes the run method. 

Notice that I’m passing the command line arguments into the constructor. I do this to make testing the HelloWorld class much easier than if I didn’t.

index.js

'use strict'

const HelloWorld = require('./app/helloworld');

let c = new HelloWorld(process.argv.slice(2));
c.run();

HelloWorld is simple.  If no command line args are passed, the run method will log the greeting.  If args are passed, they will be concatenated and then logged.

helloWorld.js

'use strict'

const Logger = require('./logger');

class HelloWorld {

    constructor (commandLineArgs) {
        this.commandLineArgs = commandLineArgs;
        this.greeting = 'Hello World';
        this.logger = new Logger();
    }

    run() {
        if (this.commandLineArgs && this.commandLineArgs.length) {
            this.logger.log(this.commandLineArgs.join(' '));
        } else {
            this.logger.log(this.greeting);
        }
    }
}

module.exports = HelloWorld;

Logger outputs message to the console. I always create a logger for my Nodejs apps, so other classes don’t need console.log, etc.  I like the object oriented approached to keep my code clean and familiar. This is a very simple Logger class, enhance it as required for your apps.

logger.js

'use strict'

class Logger {

    log(message) {
        console.log(message);
    }
}

module.exports = Logger;

Unit Testing Setup

For my Nodejs projects I use the following testing tools:

  • Mocha – unit test framework
  • Chai – BDD / TDD assertion library
  • Istanbul – code coverage tool
  • Sinon – standalone test spies, stub, and mock framework
  • Sinon Chai – extends Chai with assertions for Sinon.

You can use Mocha by itself or Mocha and Istanbul to get coverage.  I like the features of Chai, but at the end of the day, it’s personal preference for testing style.  “Actually testing is critical, test style is not.”

I install the test tools locally in my Nodejs projects rather than globally so that I can have multiple versions of a tool if required. Local installs make the command line longer, but that’s not an issue since the command will be in package.json or in a gulp task, bottom line, you don’t have to type it.

Local install example:  npm install mocha –save-dev

Understanding Mocha Setup and Startup

Node and npm commands are executed from your root folder.

When Mocha is invoked, by default it will look in the /test folder for the mocha.opts file, which is the Mocha configuration file. 

mocha.opts

The first line tells Mocha which folder to look into for the tests, if not supplied, it will use the /test folder.  I’ve chosen the /app folder because I like to have my unit tests in the same folder as the JavaScript being tested.

The second line loads up the common.js file

The third line tells Mocha to look not only in the app folder but also all sub-folders.

Finally, the fourth line, tell Mocha to quit processing when a test fails. 

Note:  When running your full test suite, or when running on a CI server, the bail option is probably not appropriate.

app
--require ./test/common.js
--recursive
--bail

common.js

This setup is optional, but its value is that I don’t have to repeat these require statements for Chai and Sinon in every test js file .

'use strict';

global.chai = require('chai');
global.chai.should();

global.expect = global.chai.expect;
global.sinon = require('sinon');

global.sinonChai = require('sinon-chai');
global.chai.use(global.sinonChai);

package.json scripts section

The scripts section of the package.json file makes it easy to run commands, especially commands with long text.

To run a command from the terminal or command line type:  npm run {script command name}

For example,  npm run example or npm run tdd:mac

The example and example2 command run the app with and without command line arguments.

The test command runs all the unit tests and code coverage report.

The tdd:mac command runs Mocha and all your tests.  Then it begins to watch the target folder for any changes.  When a file changes, its reruns the tests automatically.

Note:  mocha -w on Windows does not work, hence the command tdd:mac.  Bugs have been logged.  For now, if you’re on Windows, I recommend writing a glup task that watches the folder and then runs mocha without the -w option.  Optionally, if you’re a WebStorm user, you can set this up in the WebStorm IDE if desired.

My typical workflow on my Mac is to open Atom or WebStorm, view my spec file and code being tested in split view, then in a terminal window I run, npm run tdd:mac and I’m good to go.  I get instant feedback from my Mocha test runner as a write tests or code.

"scripts": {
    "example": "node index.js",
    "example2": "node index.js Hey world!",
    "test": "./node_modules/.bin/istanbul cover ./node_modules/mocha/bin/_mocha",
    "tdd:mac": "./node_modules/.bin/mocha -w"
  },

logger.spec.js

This unit test verifies that the Logger class will invoke console.log and pass the correct message to it.

When you’re unit tests actually write to the console, the text will be outputted into your Mocha report stream outputted to the console.  To limit the noise, I’ve created the below TestString that blends in nicely with the Mocha report.

The variable ‘sut’ is an acronym for ‘system under test.’  I use ‘sut’ to make it easy for the next person reading my tests to quickly see what object is being tested. Consistent code is much easy to read and maintain.

The Sinon library makes it easy to test class dependencies by either spying, stubbing, or mocking the class or methods.  The reason I don’t use a stub or mock here for console.log is because it will block the Mocha report from being displayed.  The spy was a good fit and the TestString gave me the output I wanted.

'use strict'

const Logger = require('./logger');
const TestString = '    ✓';  // nice hack to keep the mocha report clean. LOL.

describe('Logger', () => {
    it('should log a message to the console', () => {
        let sut = new Logger();
        let spy = sinon.spy(console, 'log');

        sut.log(TestString);

        expect(spy.calledOnce);
        expect(spy.calledWithMatch(TestString));

        spy.restore();
    });
});

helloWorld.spec.js

To limit bugs and typo’s I use constants for my expected results and method arguments.

In this simple app, the Logger is exposed as a property on HelloWorld, making it accessible for stubbing at test time.  In a larger app, the Logger would be an injected dependency.  Injected dependencies are a no brainer to stub and mock.

'use strict'

const HelloWorld = require('./helloWorld');
const Logger = require('./logger');
const DefaultGreeting = 'Hello World';
const Arg1 = 'Hello';
const Arg2 = 'there!'

describe('HelloWorld', () => {

    describe('Constructor', () => {

        it('should be created with three properties: commandLineArgs, greeting, and logger', () => {
            let sut = new HelloWorld();
            expect(sut).to.have.property('commandLineArgs');
            expect(sut).to.have.property('greeting');
            expect(sut).to.have.property('logger');
        });

        it('should have default greeting', () => {
            let sut = new HelloWorld();
            expect(sut.greeting).to.equal(DefaultGreeting);
        });

        it('should have command line args set when supplied', () => {
            let sut = new HelloWorld([Arg1, Arg2]);
            expect(sut.commandLineArgs).to.have.lengthOf(2);
            expect(sut.commandLineArgs[0]).to.equal(Arg1);
            expect(sut.commandLineArgs[1]).to.equal(Arg2);
        });
    });

    describe('Run', () => {
        it('should log command line args when supplied', () => {
            let logger = new Logger();
            let stub = sinon.stub(logger, 'log').returns();
            let sut = new HelloWorld([Arg1, Arg2]);
            sut.logger = logger;

            sut.run();

            expect(logger.log).to.have.been.calledOnce;
            expect(logger.log).to.have.been.calledWith(`${Arg1} ${Arg2}`);

            stub.restore();
        });

        it('should log default greeting when no command line args are passed', () => {
            let logger = new Logger();
            let stub = sinon.stub(logger, 'log').returns();
            let sut = new HelloWorld();
            sut.logger = logger;

            sut.run();

            expect(logger.log).to.have.been.calledOnce;
            expect(logger.log).to.have.been.calledWith(DefaultGreeting);

            stub.restore();
        });
    });

});

Test Results

Executing npm test or npm run test, produces the following output.

The describe and it blocks are nicely nested in this Istanbul coverage report.

The first item in the Logger group is a black check mark, this is my little hack I mentioned above in logger.spec.js file test.

Atom Snippets

Atom editor snippets rock.  The very best snippet documentation I’ve read is here , read it and you’ll be a happy camper.

These snippets assist my coding of classes and unit tests.

'.source.js':
  'Fat Arrow':
    'prefix': 'fat'
    'body': '() => {}'
  'describe unit test':
    'prefix': 'dsc'
    'body': """
        describe('$1', () => {
            $2
        });

    """
  'it unit test':
    'prefix': 'itt'
    'body': """
        it('should $1', () => {
            $2
        });

    """
  'Class with Constructor':
    'prefix': 'cctor'
    'body': """
        'use strict'

        class $1 {

            constructor () {
                $2
            }
        }

        module.exports = $1;
    """
  'Method Maker':
    'prefix': 'mm'
    'body': """
        $1($2) {
            $3
        }

    """

Download

https://github.com/Oceanware/nodejs-es6-mocha

Close

I hope this information helps you in setting up a Nodejs project that uses ES6, Mocha, Chai, and Istanbul.

Just a grain of sand on the worlds beaches.





About List