Testing Node.js Applications

@thanpolas

Who is Thanasis

Professional

Community

Open Source

  • Eng Mgr at Waldo
  • CTO at QallOut
  • CTO at Insight Replay
  • Founder
  • Over 40 NPM packages
  • Contributor to major Node.js packages
  • Avid OSS author
  • Software Engineer, CTO, founder
  • Recently moved to London from Greece
  • Available for hire

Who is this presentation for?

  • Early - Mid stage startups
  • Personal Projects
  • Boilerplates

Today's Menu

  1. Testing tools & libraries
  2. How to setup your Node.js Application
  3. Testing directory structure and overview
  4. Walkthrough of setting up test cases

Testing Tools & Libraries

These are the tools I use and can talk about, tools can change, patterns remain the same...

Setting up your Node.js App

  • Avoid initializing anything outside of functions.
  • Wrap your whole application in a single boot entry point.

Setting up your Node.js App

const postgreService = require('./services/postgre.service');
const webService = require('./services/web.service');

const app = module.exports = {};

app.init = Promise.method(function () {
    return Promise.all([
        postgreService.init(),
        // ... any other low-level / datastore service
    ])
        .then(function () {
            return Promise.all([
                webService.init(),
                // ... other high-level services
            ]);
        });
});

// Determine if module was the execution entry point
const isStandAlone = require.main === module;

if (isStandAlone) {
  app.init();
}

index.js

Application Boot

Standalone Test

Ignition

Tests Directory Structure

- /test/
|-- asserts/
|-- e2e/
|-- fixtures/
|-- lib/
|-- unit/

test/asserts/

Perform standardized tests on your models

Tests Directory Structure

  • Test expected properties of Object
  • Test expected types of Object
  • Test expected values of Object

test/asserts/

Tests Directory Structure

const chai = require('chai');
const expect = chai.expect;

const itemTests = module.exports = {};

itemTests.runAll = function (item) {
  itemTests.testProperties(item);
  itemTests.testTypes(item);
  itemTests.testValues(item);
};

Part 1/2

test/asserts/

Tests Directory Structure

itemTests.testProperties = function (item) {
  expect(item).to.be.an('object');

  expect(item).to.have.keys([
    'id',
    'name',
  ]);
};

itemTests.testTypes = function (item) {
  expect(item.id).to.be.a('string');
  expect(item.name).to.be.a('string');
};

itemTests.testValues = function (item) {
  expect(item.id.length).to.equal(12);
  expect(item.name).to.match(/^[\w]{3,8}$/);
};

Part 2/2

test/fixtures/

Tests Directory Structure

  • Easy creation of input data.
  • Use faker for randomizing data.
  • Definitely not idempotent functions.

test/fixtures/

Tests Directory Structure

test/fixtures/account.fix.js

const faker = require('faker');

const phoneFix = require('./phone-numbers.fix');

const accountFix = module.exports = {};

accountFix.minFields = () => ({
  name: faker.name.firstName(),
  phone_number: phoneFix.getUS(),
});

test/lib/

Tests Directory Structure

  • Contains master test boot "test.lib.js" (more next).

  • Libraries to perform all e2e requests per model.
    • As stand-alone methods.
    • As Mocha Setup Cases.

test/lib/test.lib.js

Tests Directory Structure

  • Is included by all tests.
  • Has two master methods to boot for e2e and unit.
    • e2e practically boots up your Node.js App.
    • Unit boot prepares anything you need to have ready.
  • Contains any commonly used helpers.

test/lib/account.lib.js

Tests Directory Structure

  • Provide methods to create account[s] of all types (?).
  • Provide methods to setup mocha with new accounts.

test/lib/account.lib.js

Tests Directory Structure

const axios = require('axios');

const accountFix = require('../fixtures/account.fix');

const accountLib = module.exports = {};

accountLib.setupOne = function() {
    beforeEach(function () {
        const accountData = accountFix.one();
        return accountLib.create(accountData)
            .then((accountRecord) => {
                this.accountOne = accountRecord;
            });
    });    
};

accountLib.create = function (accountData) {
    return axios.post('/account/', accountData)
        .then(function (res) {
            return res.data;
        });
}

Putting it all together

Test Account Creation

Putting it all together

const expect = require('chai').expect;

const testLib = require('../lib/test.lib');
const accountFix = require('../fixtures/account.fix');
const accountLib = require('../lib/account.lib');
const accountAssert = require('../asserts/account.assert');

describe('Account Create', function () {
  testLib.init();

  describe('Nominal behaviors', function() {
    it('Should create an account and get expected outcome', function () {
      const accountData = accountFix.one();
      return accountLib.create(accountData)  
        .then((accountRecord) => {
            accountAssert.runAll(accountRecord);
        });
    });
  });
});

Test Account Creation

Putting it all together

Important points:

  • Wrap all your test cases in a single "describe" statement, so you can easily skip the whole suite.
  • Wrap your tests in double "describe" statements, you will need it to better setup your cases (more on that later).

Test Account Creation

Putting it all together

const expect = require('chai').expect;

const testLib = require('../lib/test.lib');
const accountLib = require('../lib/account.lib');
const eventsFix = require('../fix/events.fix');
const eventsLib = require('../lib/events.lib');
const eventsAssert = require('../asserts/events.assert');

describe('Create Event', function () {
  testLib.init();

  describe('Nominal behaviors', function() {
    accountLib.setupOne();

    it.only('Should create an account and get expected outcome', function () {
      const eventData = eventFix.one();
      return eventLib.create(this.accountOne.id, eventData)  
        .then((eventRecord) => {
            eventAssert.runAll(eventRecord);
        });
    });
  });
});

Test Account Creation

Putting it all together

More points:

  • Avoid inlining actual XHR calls in tests.
    • Make your helper methods flexible.
    • Reserve that only for very edge cases.
  • Beware of context.

To Summarise

  1. Invoke tester library INIT to boot service.
  2. Run all setup instructions to bring service to desired state.
  3. Run test case.
  4. Optionally run the outcome through your automated asserters.

Flow of running tests

Thank you

Thanasis Polychronakis

@thanpolas

https://speakerdeck.com/thanpolas

Questions?

Thanasis Polychronakis

@thanpolas

https://speakerdeck.com/thanpolas

Testing a Node.js Application

By thanpolas

Testing a Node.js Application

  • 974