Testing Javascript

How to stop worrying and start testing

Nordnet Academy


jQuery('a.submit').on('click', function() {
    // do something useful
});
  • Coupled with DOM and jQuery
  • Runs in a browser, hard to automate
  • Manual test all the things!

Testing JS


(function() {
  'use strict';

  angular
    .module('mobileapp')
    .controller('SettingsController', SettingsController);

  /* @ngInject */
  function SettingsController(storageservice) {
    var vm = this;
    vm.country = storageservice.get('country');
  }
})();

Testing JS

Testing JS

  • More complex frameworks
  • More complex applications
  • Complicated manual testing
  • Harder to make changes

Testing JS

  • Improve quality
  • Faster feedback
  • Keep old code working
  • Documentation

Frameworks

Frameworks

  • sinon
    • spies, stubs, mocks
  • chai
    • assertion library

Test structure

describe('what function/lib/file is being tested', () => {

  describe('when - given a certain condition', () => {

    // use to set up Given and When
    beforeEach(() => {
      // set up test conditions
    });

    // use to assert results - Then
    it('should result in something', () => expect(something).to.be.defined);

    afterEach(() => {
      // clean up
    });
  });  
});

Mocha

describe('tests', 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
});

Mocha

describe('tests', function() {

  beforeEach(function() {
    // beforeEach hook
  });

  beforeEach(function namedFunction() {
    // beforeEach:namedFunction
  });

  beforeEach('some description', function() {
    // beforeEach:some description
  });
});

Mocha

describe('tests', function() {

  beforeEach(function(done) {
    asyncPrepare().then(done);
  });

  it('should execute async operation', function(done) {
    asyncOperation.then(response => {
      expect(response).to.be.ok;
      done();
    });
  });
});

Sinon - spy

  • Function that records
    • arguments
    • return value
    • this
    • exceptions thrown
  • for all calls
// anonymous function spy
const spy = sinon.spy();

// spy for given function
const spySomeFunc = sinon.spy(someFunc);

// spy on specific method
const spyGet = sinon.spy(api, 'get');

Sinon - stub

  • Functions with pre-programmed behavior
  • Full support for spy API
  • Additional methods to alter stub behavior
  • Force code flow to a specific path
  • Avoid calling the real function

Sinon - stub

// anonymous function stub
const stub = sinon.stub();

// replace function with stub
const stubGet = sinon.stub(api, 'get');

// replace function with new function wrapped in a spy
const stubGetResolved = 
    sinon.stub(api, 'get', () => Promise.resolve({ answer: 42 }));

// restore original method - important to clean up!
api.get.restore();

Sinon - mocks

  • Functions with pre-programmed behavior and expectations
  • Fails test is not used as defined by expectations
  • Full support for spy and stub APIs

Sinon - sandbox

  • Simplifies restoring fakes after tests
    • fake XHR, timers, spies, stubs
  • sinon.sandbox.create()
  • sandbox.restore()

Sinon - more tools

  • Fake timers
  • Fake XHR
  • Fake server
  • Assertions
  • Matchers

Chai

  • Assertion library
  • BDD or TDD style
  • assert()
  • expect()
  • should()

Chai


expect(3).to.equal(3);
expect(1).to.not.be.true;

expect(someObj).to.be.defined;

// deep equal
expect({ foo: 'bar' }).to.eql({ foo: 'bar'});
expect([1, 2, 3]).to.eql([1, 2, 3]);

// arrays
expect([1,2,3]).to.include(2);

Testing React

Testing React

  • Render into a DOM node
  • Stub dependencies
  • Verify content
  • Verify state and props
  • Verify triggers actions/events

Testing React

import React from 'react';

class NewsItem extends React.Component {
  render() {
    return (
      <li>{ this.props.newsItem.headline }</li>
    );
  }
}

NewsItem.propTypes = {
  newsItem: React.PropTypes.object.isRequired,
};

export default NewsItem;

Testing React

import React from 'react/addons';
import NewsItem from './../news-item';

// React 0.14
// import TestUtils from 'react-addons-test-utils';
const TestUtils = React.addons.TestUtils;

describe('NewsItem', () => {
  let node;
  const headline = 'abc';

  beforeEach(() => {
    const component = 
        TestUtils.renderIntoDocument(
            <NewsItem newsItem={ {headline} } />);
    node = React.findDOMNode(component);
  });

  it('should display headline', 
    () => expect(node.textContent).to.equal(headline));
  it('should render li node', 
    () => expect(node.tagName).to.equal('LI'));
});

Testing React

class NewsItem extends React.Component {
  componentDidMount() {
    this.props.mounted();
  }
  ...
}


import React from 'react/addons';
import NewsItem from './../news-item';

const TestUtils = React.addons.TestUtils;

describe('NewsItem', () => {
  let spy;

  beforeEach(() => {
    spy = sinon.spy();
    const component = 
        TestUtils.renderIntoDocument(
            <NewsItem newsItem={} mounted={ spy } />);
    node = React.findDOMNode(component);
  });

  it('should call mounted', () => expect(spy).to.have.been.called);
});

Testing React

class NewsItem extends React.Component {
  render() {
    return (
      <a onClick={ this.props.clicked }>{ this.props.newsItem.headline }</a>
    );
  }
}


import React from 'react/addons';
import NewsItem from './../news-item';

const TestUtils = React.addons.TestUtils;

describe('NewsItem', () => {
  let node;
  let spy;

  beforeEach(() => {
    spy = sinon.spy();
    node = renderComponent(
            <NewsItem newsItem={} clicked={ spy } />);
    TestUtils.Simulate.click(node);
  });

  it('should call clicked', () => expect(spy).to.have.been.called);
});

Testing React

  • TestUtils docs
    • more methods in the API
  • Differences between 0.13 and 0.14
    • react-addons-test-utils
    • react-dom

End-to-End Testing

Nightwatch.js

Nightwatch.js

  • nightwatch.json
    • basic configuration
  • nightwatch.js

    • starts up tests

  • e2e-tests/

    • test files

    • page objects

Page objects

// e2e-tests/page-objects/news-page.js
export default {
  url: 'http://localhost:9000',
  elements: {
    getMoreNewsButton: {
      selector: 'button'
    }
  }
}

e2e tests

/* e2e-tests/tests/get-more-news-button.js */
const beforeEach = (client) => {
  const newsPage = client.page['news-page']();
  newsPage
    .navigate()
    .waitForElementVisible('body', 1000);
};

const displaysGetMoreNewsButton = (client) => {
  client.expect.element('button').to.be.present;
  client.expect.element('button').to.contain.text('Get more news');
  client.end();
};

export default {
  beforeEach,
  'Get more news button is displayed': displaysGetMoreNewsButton,
};

Commands

// e2e-tests/page-objects/news-page.js
const commands = {
  loadMore: function() {
    return this.click('button');
  },
};

export default {
  url: 'http://localhost:9000',
  commands: [commands],
  elements: {
    getMoreNewsButton: {
      selector: 'button'
    }
  }
}

e2e tests

/* e2e-tests/tests/get-more-news-button.js */
const url = 'http://localhost:9000/';
let newsPage;

const beforeEach = client => {
  newsPage = client.page['news-page']();
  newsPage
    .navigate()
    .waitForElementVisible('body', 1000);
};

const loadsMoreNewsOnButtonClick = client => {
  newsPage.loadMore();
  // TOOD await and assert
  client.end();
};

export default {
  beforeEach,
  'Loads more news on button click': loadsMoreNewsOnButtonClick,
};

Testing JS

Questions?

Made with Slides.com