JavaScript Unit Testing

DevTalk 16-03-17

Tests?

What is software testing?

  • Testing is the process of establishing confidence that a program or system does what it is supposed to (Hetzel, 1973).
  • Testing is the process of executing a program or system with the intent of finding errors (Myers, 1979).
  • Testing is demonstrating that a system is fit for purpose (Evans, et al, 1996)
  • Blah, blah, blah...

Type of tests

  • Functionality Testing

  • Usability testing

  • Interface testing

  • Compatibility testing

  • Performance testing

  • Security testing

  • Unit testing

  • Integration testing

  • ...

Unit tests

ensure that individual components of the app work as expected. Assertions test the component API.

Test hierarchy

The goal of unit testing is:

  • to isolate each part of the program,
  • show that the individual parts are correct.

A unit test should NOT:

  • access the network,
  • hit a database,
  • use the file system,
  • call other non-trivial components,
  • .... no side-effects in unit tests.

Mocha

What is this?

Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases.

Example of installation and run

  • npm install mocha
  • example file to test:
  •  
  •  
  •  
  •  
  •  
  • in package.json:
  • dsf
  • sd
  • npm test
var assert = require('assert');
describe('Array', function() {
  describe('#indexOf()', function() {
    it('should return -1 when the value is not present', function() {
      [1,2,3].indexOf(4).should.equal(-1)
    });
  });
});
"scripts": {
    "test": "mocha"
  }

Assertions

  • should.js - BDD style shown throughout these docs

  • expect.js - expect() style assertions

  • chai - expect(), assert() and should-style assertions

  • better-assert - C-style self-documenting assert()

  • unexpected - “the extensible BDD assertion toolkit”

Interfaces

describe('#indexOf()', function() {
    context('when not present', function() {
        it('should not throw an error', function() {
            specify('if is equal to 4', function() {
            });
        });
    });
});
  • describe() - header for a section of test

  • context() - is just an alias for describe(), and behaves the same way; it just provides a way to keep tests easier to read and organized.

  • it() - for a specific testing concern

  • specify() - similar as context to describe but to it

Asynchronous code

  • callback:

describe('User', function() {
  describe('#save()', function() {
    it('should save without error', function(done) {
      var user = new User('Luna');
      user.save(done);
    });
  });
});
  • promise:
beforeEach(function() {
  return db.clear()
    .then(function() {
      return db.save([tobi, loki, jane]);
    });
});

describe('#find()', function() {
  it('respond with matching records', function() {
    return db.find({ type: 'User' }).should.eventually.have.length(3);
  });
});

Hooks

describe('hooks', function() {

  before(function() {
    // runs before all tests in this block
  });

  after(function() {
    // runs after all tests in this block
  });

  beforeEach(function() {
    // runs before each test in this block
  });

  afterEach(function() {
    // runs after each test in this block
  });

  // test cases
});

Exclusive tests

describe('Array', function() {
  describe('#indexOf()', function() {
    it.only('should return -1 unless present', function() {
      // this test will be run
    });

    it('should return -1 if called with a non-Array context', function() {
      // this test will not be run
    });
  });
});

describe('Array', function() {
  describe.only('#indexOf()', function() {
    it('should return -1 unless present', function() {
      // this test will be run
    });

    it('should return the index when present', function() {
      // this test will also be run
    });
  });

Inclusive tests

describe('Array', function() {
  describe.skip('#indexOf()', function() {
    // ...
  });
  
  xdescribe('#indexOf()', function() {
    xit('x simple', () => {
      // ...
    });

    it('simple', () => {
      // ...
    });
  });
});

it('should only test in the correct environment', function() {
  if (/* check test environment */) {
    // make assertions
  } else {
    this.skip();
  }
});

Mocha with bundlers

  • Webpack: https://www.npmjs.com/package/mocha-webpack
  • Grunt: https://github.com/kmiyashiro/grunt-mocha
  • Gulp: https://github.com/sindresorhus/gulp-mocha

Chai

What is this?

Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.

Chai interfaces

  • Should extends each object with a should property to start your chain.
chai.should();

foo.should.be.a('string');
foo.should.equal('bar');
foo.should.have.lengthOf(3);
tea.should.have.property('flavors').with.lengthOf(3);

Chains:

to, be, been, is, that, which, and, has, have, with, at, of, same

Chai interfaces

  • Expect is function.  After function call you can start your chain.
var expect = chai.expect;

expect(foo).to.be.a('string');
expect(foo).to.equal('bar');
expect(foo).to.have.lengthOf(3);
expect(beverages).to.have.property('tea').with.lengthOf(3);

Chains:

to, be, been, is, that, which, and, has, have, with, at, of, same

Chai interfaces

  • Assert provides the classic assert-dot notation, similar to that packaged with node.js.
var assert = require('chai').assert
  , foo = 'bar'
  , beverages = { tea: [ 'chai', 'matcha', 'oolong' ] };

assert.typeOf(foo, 'string'); // without optional message
assert.typeOf(foo, 'string', 'foo is a string'); // with optional message
assert.equal(foo, 'bar', 'foo equal `bar`');
assert.lengthOf(foo, 3, 'foo`s value has a length of 3');
assert.lengthOf(beverages.tea, 3, 'beverages has 3 types of tea');

Plugins

  • Chai Match

npm i --save-dev chai-match

var chai = require('chai');
chai.use(require('chai-match'));

expect('some thing to test').to.match(/some (\w+) to test/).and.capture(0).equals('thing');
  • Chai Deep Match

npm install --save chai-deep-match

var chai = require('chai');
var chaiDeepMatch = require('chai-deep-match');

chai.use( chaiDeepMatch );

chai.expect( { a: 'foo', b: 'bar', c: 'baz' } ).to.deep.match( { a: 'foo', c: 'baz' } );
  • Chai Immutable

  • and others

Sinon.JS

Sinon.js is a vital tool when writing JavaScript unit tests. Works with any unit testing framework.

 

Sinon allows you to replace the difficult parts of your tests with something that makes testing simple.

 

Mainly you can use:

  • spies
  • stubs
  • mocks

Spies

Which offer information about function calls, without affecting their behavior.

 

A test spy is a function that records arguments, return value, the value of this and exception thrown (if any) for all its calls.

Spies

function setupNewUser(info, callback) {
  var user = {
    name: info.name,
    nameLowercase: info.name.toLowerCase()
  };

  try {
    Database.save(user, callback); //send ajax request
  }
  catch(err) {
    callback(err);
  }
}

We can check how many times a function was called


it('should call save once', function() {
  var save = sinon.spy(Database, 'save');

  setupNewUser({ name: 'test' }, function() { });

  save.restore();
  sinon.assert.calledOnce(save);
});
it('should pass object with correct values to save', function() {
  var save = sinon.spy(Database, 'save');
  var info = { name: 'test' };
  var expectedUser = {
    name: info.name,
    nameLowercase: info.name.toLowerCase()
  };

  setupNewUser(info, function() { });

  save.restore();
  sinon.assert.calledWith(save, expectedUser);
});

We can check what arguments were passed to a function 

Stubs

Which are like spies, but completely replace the function. This makes it possible to make a stubbed function do whatever you like — throw an exception, return a specific value, etc.

 

Test stubs are functions (spies) with pre-programmed behavior.

 

They support the full test spy API in addition to methods which can be used to alter the stub’s behavior.

 

it('should pass object with correct values to save', function() {
  var save = sinon.stub(Database, 'save');
  var info = { name: 'test' };
  var expectedUser = {
    name: info.name,
    nameLowercase: info.name.toLowerCase()
  };

  setupNewUser(info, function() { });

  save.restore();
  sinon.assert.calledWith(save, expectedUser);
});

Stubs can be used to replace problematic code

Stubs can also be used to trigger different code paths

it('should pass the error into the callback if save fails', function() {
  var expectedError = new Error('oops');
  var save = sinon.stub(Database, 'save');
  save.throws(expectedError);
  var callback = sinon.spy();

  setupNewUser({ name: 'foo' }, callback);

  save.restore();
  sinon.assert.calledWith(callback, expectedError);
});

Thirdly, stubs can be used to simplify testing asynchronous code.


it('should pass the database result into the callback', function() {
  var expectedResult = { success: true };
  var save = sinon.stub(Database, 'save');
  save.yields(null, expectedResult);
  var callback = sinon.spy();

  setupNewUser({ name: 'foo' }, callback);

  save.restore();
  sinon.assert.calledWith(callback, null, expectedResult);
});

Mocks

Which make replacing whole objects easier by combining both spies and stubs

 

Mocks (and mock expectations) are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations.

 

A mock will fail your test if it is not used as expected.

it('should pass object with correct values to save only once', function() {
  var info = { name: 'test' };
  var expectedUser = {
    name: info.name,
    nameLowercase: info.name.toLowerCase()
  };
  var database = sinon.mock(Database);
  database.expects('save').once().withArgs(expectedUser);

  setupNewUser(info, function() { });

  database.verify();
  database.restore();
});

Mocks should be used primarily when you would use a stub, but need to verify multiple more specific behaviors on it.

Enzyme

Shallow Rendering

import { shallow } from 'enzyme';

describe('<MyComponent />', () => {
  it('should render three <Foo /> components', () => {
    const wrapper = shallow(<MyComponent />);
    expect(wrapper.find(Foo)).to.have.length(3);
  });

  it('simulates click events', () => {
    const onButtonClick = sinon.spy();
    const wrapper = shallow(
      <Foo onButtonClick={onButtonClick} />
    );
    wrapper.find('button').simulate('click');
    expect(onButtonClick.calledOnce).to.equal(true);
  });
});

Shallow rendering is useful to constrain yourself to testing a component as a unit, and to ensure that your tests aren't indirectly asserting on behavior of child components.

Shallow Rendering (2)

Simple shallow calls:

 - constructor,

 - render

Shallow + setProps calls:

 - componentWillReceiveProps,

 - shouldComponentUpdate,

 - componentWillUpdate,

 - render

Shallow + unmount calls:

 - componentWillUnmount

Full Rendering

import { mount } from 'enzyme';
import sinon from 'sinon';
import Foo from './Foo';

describe('<Foo />', () => {
  it('calls componentDidMount', () => {
    sinon.spy(Foo.prototype, 'componentDidMount');
    const wrapper = mount(<Foo />);
    expect(Foo.prototype.componentDidMount.calledOnce).to.equal(true);
  });

  it('allows us to set props', () => {
    const wrapper = mount(<Foo bar="baz" />);
    expect(wrapper.props().bar).to.equal("baz");
    wrapper.setProps({ bar: "foo" });
    expect(wrapper.props().bar).to.equal("foo");
  });
});

Full DOM rendering is ideal for use cases where you have components that may interact with DOM APIs, or may require the full lifecycle in order to fully test the component (i.e., componentDidMount etc.)

Full Rendering (2)

Simple mount calls

 - constructor
 - render
 - componentDidMount

Mount + setProps calls

 - componentWillReceiveProps

 - shouldComponentUpdate

 - componentWillUpdate

 - render

 - componentDidUpdate

Mount + unmount calls

 - componentWillUnmount

Static Rendering

import { render } from 'enzyme';

describe('<Foo />', () => {
  it('renders three `.foo-bar`s', () => {
    const wrapper = render(<Foo />);
    expect(wrapper.find('.foo-bar')).to.have.length(3);
  });

  it('rendered the title', () => {
    const wrapper = render(<Foo title="unique" />);
    expect(wrapper.text()).to.contain("unique");
  });
});

Enzyme's render function is used to render react components to static HTML and analyze the resulting HTML structure.

only calls render but renders all children

Summary

 - Always begin with shallow

 - If componentDidMount or componentDidUpdate should be tested, use mount

 - If you want to test component lifecycle and children behavior, use mount

 - If you want to test children rendering with less overhead than mount and you are not interested in lifecycle methods, use render

React

Components

export default class Simple extends PureComponent {
  static propTypes = {
    data: PropTypes.object,
    simpleFlag: PropTypes.bool,
    getSimpleData: PropTypes.func.isRequired
  };

  componentWillMount() {
    this.props.getSimpleData(this.props.simpleFlag);
  }

  render() {
    return (
      <div className="simple">

        {this.props.simpleFlag ?
          <h1 className="simple__title">
            <FormattedMessage {...messages.title} />
          </h1>
        : null}

        <SimpleList data={this.props.data} />
      </div>
    );
  }
}
import React from 'react';
import { expect } from 'chai';
import { shallow } from 'enzyme';
import { spy } from 'sinon';
import { FormattedMessage } from 'react-intl';

import Simple from '../simple.component';
import SimpleList from '../simpleList/simpleList.component';


describe('Simple: Component', () => {
  const defaultProps = {
    getSimpleData: () => {},
    data: {a: 'a', b: 'b'},
  };

  const component = (props = {}) => (
    <Simple {...defaultProps} {...props} />
  );

  it('should render Simple root', () => {
    const wrapper = shallow(component());
    expect(wrapper.find('.simple'))
      .to.have.length(1);
  });

  it('should dispatch getSimpleData action on mount', () => {
    const getSimpleData = spy();
    shallow(component({ getSimpleData }));

    expect(getSimpleData.calledOnce)
      .to.be.equal(true);
  });

  it('should dispatch getSimpleData action with proper arguments', () => {
    const getSimpleData = spy();
    shallow(component({ getSimpleData }));

    expect(getSimpleData.getCall(0).args[0])
      .to.be.equal(defaultProps.simpleFlag);
  });

  it('should pass data into <SimpleList />', () => {
    const wrapper = shallow(component());
    expect(wrapper.find(SimpleList).prop('data'))
      .to.be.equal(defaultProps.data);
  });

  describe('Message Rendering: ', () => {
    it('should render message', () => {
      const wrapper = shallow(component({ simpleFlag: true }));
      expect(wrapper.find(FormattedMessage))
        .to.have.length(1);
    });

    it('should not render message', () => {
      const wrapper = shallow(component({ simpleFlag: false }));
      expect(wrapper.find(FormattedMessage))
        .to.have.length(0);
    });
  });
});

Redux

introduction

 

Action

Reducer

Store

View

Actions

 

import { SUBMIT, SUBMIT_SUCCESS, SUBMIT_FAIL } from './simple.constants';


export function submit( email, locale ) {
  return {
    type: SUBMIT,
    payload: { email, locale },
  };
}

export function errorSubmitData() {
  return {
    type: SUBMIT_FAIL,
  };
}

export function successSubmitData() {
  return {
    type: SUBMIT_SUCCESS,
  };
}
import { expect } from 'chai';
import { SUBMIT, SUBMIT_SUCCESS, SUBMIT_FAIL } from '../simple.constants';
import { submit, errorSubmitData, successSubmitData } from '../simple.actions';

describe('Simple: actions', () => {
  const email = 'simple@test.com';
  const locale = 'en';

  describe('actions on SUBMIT', () => {
    it('should return SUBMIT type', () => {
      expect(submit().type).to.equal(SUBMIT);
    });

    it('should return payload', () => {
      expect(submit(email, locale).payload).to.deep.equal({email, locale});
    });
  });

  describe('actions on SUBMIT_FAIL', () => {
    it('should return SUBMIT_FAIL type', () => {
      expect(errorSubmitData().type).to.equal(SUBMIT_FAIL);
    });
  });

  describe('actions on SUBMIT_SUCCESS', () => {
    it('should return SUBMIT_SUCCESS type', () => {
      expect(successSubmitData().type).to.equal(SUBMIT_SUCCESS);
    });
  });
});

Reducers

import { Record } from 'immutable';
import { SUBMIT, SUBMIT_FAIL, SUBMIT_SUCCESS } from './simple.constants';

const StateRecord = new Record({
  data: null,
  isLoading: false,
  isSent: null,
});

const initialState = new StateRecord({});

function simpleReducer(state = initialState, action) {
  switch (action.type) {
    case SUBMIT:
      return state
        .set('isLoading', true);
    case SUBMIT_FAIL:
      return state
        .set('isLoading', false)
        .set('isSent', false);
    case SUBMIT_SUCCESS:
      return state
        .set('isLoading', false)
        .set('isSent', true);
    default:
      return state;
  }
}

export default simpleReducer;
import { expect } from 'chai';
import { fromJS } from 'immutable';

import reducer from '../simple.reducer';


import { SUBMIT, SUBMIT_FAIL, SUBMIT_SUCCESS } from '../simple.constants';

describe('Simple: reducer', () => {
  const state = fromJS({
    data: null,
    isLoading: false,
    isSent: null,
  });

  it('should return state on unknown action', () => {
    expect(reducer(state, { type: 'unknown-action' })).to.be.equal(state);
  });

  describe('states on SUBMIT', () => {
    it('should return state on SUBMIT', () => {
      const isLoading = true;
      const expectedState = state.set('isLoading', isLoading);
      expect(reducer(state, {
        isLoading, type: SUBMIT
      }).equals(expectedState)).to.be.equal(true);
    });
  });

  describe('states on SUBMIT_FAIL', () => {
    it('should set isLoading false on SUBMIT_FAIL', () => {
      const isLoading = false;
      const expectedState = state.set('isLoading', isLoading);
      expect(reducer(state, {
        isLoading, type: SUBMIT_FAIL
      }).equals(expectedState)).to.be.equal(false);
    });

    it('should set isSent true on SUBMIT_FAIL', () => {
      const isSent = false;
      const expectedState = state.set('isSent', isSent);
      expect(reducer(state, {
        isSent, type: SUBMIT_FAIL
      }).equals(expectedState)).to.be.equal(true);
    });
  });

  describe('states on SUBMIT_SUCCESS', () => {
    it('should set isLoading false on SUBMIT_SUCCESS', () => {
      const isLoading = false;
      const expectedState = state.set('isLoading', isLoading);
      expect(reducer(state, {
        isLoading, type: SUBMIT_SUCCESS
      }).equals(expectedState)).to.be.equal(false);
    });

    it('should set isSent true on SUBMIT_SUCCESS', () => {
      const isSent = true;
      const expectedState = state.set('isSent', isSent);
      expect(reducer(state, {
        isSent, type: SUBMIT_SUCCESS
      }).equals(expectedState)).to.be.equal(true);
    });
  });
});

Reselects

import { createSelector } from 'reselect';


const selectSimpleDomain = state => state.simpleDomain;

export const selectDomainName = createSelector(
  selectSimpleDomain, state => state.get('name')
);

export const selectData = createSelector(
  selectSimpleDomain, state => state.get('data')
);

export const filterData = createSelector(
  selectData, data => data.filter(({type}) => type === 'free')
);
import { expect } from 'chai';
import { Map } from 'immutable';

import { selectData, selectDomainName, filterData } from '../simple.selectors';

describe('Simple: selectors', () => {
  const name = 'simpleName';
  const data = [{type: 'free'}, {type:'notFree'}];
  const state = {
    simpleDomain: Map({name, data}),
  };

  describe('selectDomainName', () => {
    it('should select simple data', () => {
      expect(selectDomainName(state)).to.be.equal(name);
    });
  });

  describe('selectSimpleData', () => {
    it('should select simple data', () => {
      expect(selectData(state)).to.deep.equal(data);
    });
  });

  describe('filterData', () => {
    it('should select simple data', () => {
      expect(filterData(state)).to.deep.equal([{type: 'free'}]);
    });
  });
});

WHY TEST?

  • Reduce number of bugs
  • Tests are good documentation
  • Reduce cost of change
  • Improve design
  • Allow safe refactoring
  • Forces you to think
  • Makes development faster
  • REDUCES FEAR

A study constructed by Microsoft and IBM showed that writing tests can add 15%-35% to development time but reduce the number of bugs by

40%-90%.

Source 

Best practices

Best practices

  • Use shallow rendering whenever possible.
  • Avoid sharing Enzyme wrappers between test cases.
const wrapper = shallow(<Thing name={'Steve'} />);

it('should do stuff', () => {
  expect(wrapper).to.doStuff();
});

it('should do different stuff', () => {
  expect(wrapper).to.doDifferentStuff();
});
const defaultProps = { name: 'Steve'; };

it('should do stuff', () => {
  const wrapper = shallow(<Thing {...defaultProps} />);
  expect(wrapper).to.doStuff();
});

it('should do different stuff', () => {
  const wrapper = shallow(<Thing {...defaultProps} />);
  expect(wrapper).to.doDifferentStuff();
});

Bad

Good

Good

const defaultProps = { name: 'Steve'; };

it('should do stuff', () => {
  const wrapper = shallow(<Thing {...defaultProps} />);
  expect(wrapper).to.doStuff();
});

it('should do different stuff', () => {
  const wrapper = shallow(<Thing {...defaultProps} />);
  expect(wrapper).to.doDifferentStuff();
});

Good

// Use helper functions

const defaultProps = { name: 'Steve'; };
const component = props => (
  <Thing {...defaultProps} {...props} />;
);

it('should do stuff', () => {
  const wrapper = shallow(component());
  expect(wrapper).to.doStuff();
});

it('should do different stuff', () => {
  const wrapper = shallow(component({name: 'Medziany'});
  expect(wrapper).to.doDifferentStuff();
});

Better

Best practices

  • Only pass required props when rendering components.
const defaultProps = {
  dispatch: () => {},
  manifest: { components: [] },
};

it('should render without blowing up', () => {
  const wrapper = shallow(<Thing {...defaultProps} />);
  expect(wrapper.length).to.eql(1);
});

it('should render with additional props', () => {
  const wrapper = shallow(<Thing {...defaultProps} someProp="val" />);
  // ...
});

Best practices

  • Finding components.
// bad
it('should render checkbox', () => {
  const wrapper = shallow(<Component />);
    
  const checkbox = wrapper.find('.checkbox-class');
});

// good
it('should render checkbox', () => {
  const wrapper = shallow(<Component />);
  
  const checkbox = wrapper.find(Checkbox);
});

Best practices

  • Test inputs and outputs of components. Don’t “reach in” and test the implementation.

Best practices

In React, the input is generally one of three things:

  • Static props
  • Events generated by the user
  • A child component calling a passed-in function

The outputs in React tend to only be three things:

  • The presence of a child component
  • A call to function prop, like a redux action creator
  • Values passed into child component props

Thank you & happy testing!

Sources

Made with Slides.com