Top Banner
Unit Testing The Whys, Whens and Hows Ates Goral - Toronto Node.js Meetup - October 11, 2016
71

Unit Testing - The Whys, Whens and Hows

Apr 12, 2017

Download

Software

atesgoral
Welcome message from author
This document is posted to help you gain knowledge. Please leave a comment to let me know what you think about it! Share it to your friends and learn new things together.
Transcript
Page 1: Unit Testing - The Whys, Whens and Hows

Unit Testing

The Whys, Whens and Hows

Ates Goral - Toronto Node.js Meetup - October 11, 2016

Page 2: Unit Testing - The Whys, Whens and Hows

Ates Goral

@atesgoral

http://magnetiq.com

http://github.com/atesgoral

http://stackoverflow.com/users/23501/ates-goral

Page 3: Unit Testing - The Whys, Whens and Hows

http://myplanet.com

Page 4: Unit Testing - The Whys, Whens and Hows

Definition of a unit test

Page 5: Unit Testing - The Whys, Whens and Hows

What is a unit?

● Smallest bit of code you can test?● Talking to the actual resource may be OK if it’s stable and fast● Classic versus mockist styles (Martin Fowler)● Solitary versus sociable tests (Jay Fields)● White box versus black box testing● What’s important is the contract

http://martinfowler.com/bliki/UnitTest.html

Page 6: Unit Testing - The Whys, Whens and Hows

Inconsistent definitions

Here’s what’s common:

● Written by developers● Runs fast● Deterministic● Does not tread into integration test territory

Page 7: Unit Testing - The Whys, Whens and Hows

Appreciation of unit testing

Page 8: Unit Testing - The Whys, Whens and Hows

You don’t know unit testing until you’ve unit tested

There’s a first time for every developer. Some are more lucky than others because they ramp up in an environment that already embraces unit testing.

“But can already write flawless code when I’m in the zone.”

True. Because you’re actually running unit tests, without realizing, in your mind when you’re in the zone.

Try taking a 3 week break and see what happens to those ephemeral unit tests.

Turn those tests into unit test code so that they’re repeatable and unforgettable.

Page 9: Unit Testing - The Whys, Whens and Hows

Good unit tests

Page 10: Unit Testing - The Whys, Whens and Hows

Good unit tests

● Are functionally correct. They don’t just exercise code for the sake of exercising code.

● Don’t depend on subsequent tests -- every test runs in its own clean environment, failure of a test doesn’t bring the entire test suite down

● Run fast. You need to be able to run all of your tests as quickly and as frequently as possible. Otherwise, they lose value.

● Are actually run. Automatically. So that you don’t forget to run them.● Add new unit tests for newly discovered [and fixed] issues.

Page 11: Unit Testing - The Whys, Whens and Hows

Good code

Page 12: Unit Testing - The Whys, Whens and Hows

Good code

● Good code is more unit testable● It all comes down to good architecture and design● Planning for unit tests facilitates good code● Good encapsulation: interfaces with small surfaces, well-defined contracts,

non-leaky abstractions● Keep interdependencies low

Page 13: Unit Testing - The Whys, Whens and Hows

Good reasons

Page 14: Unit Testing - The Whys, Whens and Hows

Why and what are you unit testing?

● Misguided reasons: processes, meeting performance numbers● Testing just for testing: glue code that doesn’t have any logic, ineffective tests

that don’t actually test the functionality● Testing legacy code that is actually un-unit-testable

Be pragmatic. Don’t waste effort. Sometimes unit testing is not the answer (try end-to-end instead).

Page 15: Unit Testing - The Whys, Whens and Hows

Benefits of unit testing

Page 16: Unit Testing - The Whys, Whens and Hows

Benefits of unit testing

Benefits beyond finding bugs:

● Better code● Safety net for refactoring● Documentation of functionality (especially when in BDD style)● Prevents code from becoming an untestable entangled mass

Page 17: Unit Testing - The Whys, Whens and Hows

Test-environment-first Programming

Page 18: Unit Testing - The Whys, Whens and Hows

Be test-ready on day one

● Even if you’re not planning to add test yet● Even if there’s no code worth testing yet● Prime your environment for future unit tests● Especially, CI environment setup can be time consuming● You never know when that moment will come when you have some critical

code that needs unit testing

Do this. Please.

Page 19: Unit Testing - The Whys, Whens and Hows

Sidenote: At a bare minimum...

Even you have no time or energy to write unit tests as you go, prepare a manual test plan, and someone in your team execute them (manually) prior to releases. Bonus: share the effort as a team.

Basic smoke tests, checking for end-to-end sanity and regression.

Do this. Please.

Page 20: Unit Testing - The Whys, Whens and Hows

Basic test environment setup

Page 21: Unit Testing - The Whys, Whens and Hows

Setting up Mocha - no configuration needed

test/testNothing.js:

describe('nothing', () => {

it('should do nothing', (done) => {

done();

});

});

package.json:

"scripts": {

"test": "mocha"

},

https://mochajs.org/

npm install --save-dev mocha

npm test

nothing

✓ should do nothing

1 passing (8ms)

Page 22: Unit Testing - The Whys, Whens and Hows

Adding Chai

test/testExpectation.js:

const chai = require('chai');

const expect = chai.expect;

describe('2 + 2', () => {

it('should equal 4', () => {

expect(2 + 2).to.equal(4);

});

});

http://chaijs.com/

npm install --save-dev chai

npm test

2 + 2

✓ should equal 4

Page 23: Unit Testing - The Whys, Whens and Hows

Let’s write our first proper test

Page 24: Unit Testing - The Whys, Whens and Hows

The test

test/testArithmetic.js:

const arithmetic = require('../src/arithmetic');

describe('arithmetic', () => {

describe('.sum()', () => {

describe('when called with two numbers', () => {

it('should return their sum', () => {

expect(arithmetic.sum(2, 2)).to.equal(4);

});

});

});

});

Page 25: Unit Testing - The Whys, Whens and Hows

Implementation and run

src/arithmetic.js:

*** REDACTED ***

npm test

arithmetic

.sum()

when called with two numbers

✓ should return their sum

Page 26: Unit Testing - The Whys, Whens and Hows

Opportunistic implementation

src/arithmetic.js:

exports.sum = (a, b) => {

return 4;

};

Page 27: Unit Testing - The Whys, Whens and Hows

https://xkcd.com/221/

Page 28: Unit Testing - The Whys, Whens and Hows
Page 29: Unit Testing - The Whys, Whens and Hows

Who tests the tests?

Page 30: Unit Testing - The Whys, Whens and Hows

Test correctness

● Should not be just exercising code● Should be functionally correct● Subject to peer review?

I don’t know of any solutions to ensure test correctness.

Page 31: Unit Testing - The Whys, Whens and Hows

OH BTW

Page 32: Unit Testing - The Whys, Whens and Hows

Selectively running tests with Mocha

mocha --grep <pattern>

npm test -- --grep <pattern>

e.g.

npm test -- --grep arithmetic

Page 33: Unit Testing - The Whys, Whens and Hows

Let’s get asynchronous

Page 34: Unit Testing - The Whys, Whens and Hows

Timeout implementation

src/timeout.js:

exports.set = (callback, milliseconds) => {

setTimeout(callback, milliseconds);

};

Page 35: Unit Testing - The Whys, Whens and Hows

Timeout test

test/testTimeout.js:

it('should call the callback after the delay', (done) => {

const start = Date.now();

timeout.set(() => {

const elapsed = Date.now() - start;

expect(elapsed).to.equal(100);

done();

}, 100);

});

Page 36: Unit Testing - The Whys, Whens and Hows

Run

npm test

timeout

.set()

when called with a callback and a delay

1) should call the callback after the delay

Uncaught AssertionError: expected 105 to equal 100

+ expected - actual

-105

+100

Page 37: Unit Testing - The Whys, Whens and Hows

Flaky tests are evil

Page 38: Unit Testing - The Whys, Whens and Hows

Write deterministic tests that run fast

● Don’t rely on chance● A less than 100% pass rate is not acceptable● Don’t waste time with arbitrary delays● Use the right tools for the [right] job

Page 39: Unit Testing - The Whys, Whens and Hows

Deterministic timing

Page 40: Unit Testing - The Whys, Whens and Hows

Bring in Sinon

http://sinonjs.org/

npm install --save-dev sinon

Page 41: Unit Testing - The Whys, Whens and Hows

Use a spy and a fake timer

test/testTimeout.js:

const sinon = require('sinon');

describe('timeout', () => {

let clock = null;

beforeEach(() => {

clock = sinon.useFakeTimers();

});

afterEach(() => {

clock.restore();

});

Page 42: Unit Testing - The Whys, Whens and Hows

Use a spy and a fake timer (continued)

describe('.set()', () => {

describe('when called with a callback and a delay', () => {

it('should call the callback after the delay', () => {

const callback = sinon.spy();

timeout.set(callback, 100);

clock.tick(100);

expect(callback).to.have.been.called;

});

});

});

Page 43: Unit Testing - The Whys, Whens and Hows

Run

npm test -- --grep timeout

timeout

.set()

when called with a callback and a delay

✓ should call the callback after the delay

100% pass rate.

Page 44: Unit Testing - The Whys, Whens and Hows

Definitions of test doubles

Page 45: Unit Testing - The Whys, Whens and Hows
Page 46: Unit Testing - The Whys, Whens and Hows

Again, some inconsistencies

● Dummy● Fake● Stub● Spy● Mock

http://www.martinfowler.com/bliki/TestDouble.html

https://en.wikipedia.org/wiki/Test_double

Page 47: Unit Testing - The Whys, Whens and Hows

Test doubles - dependency injection

Page 48: Unit Testing - The Whys, Whens and Hows

Account service that takes DB as a dependency

src/accountService.js:

function AccountService(db) {

this.db = db;

}

AccountService.prototype.findById = function (accountId, callback) {

const results = this.db.querySync('account', { id: accountId });

callback(results[0]);

};

module.exports = AccountService;

Page 49: Unit Testing - The Whys, Whens and Hows

Bring in Sinon-Chai

https://github.com/domenic/sinon-chai

npm install --save-dev sinon-chai

const sinonChai = require('sinon-chai');

chai.use(sinonChai);

Page 50: Unit Testing - The Whys, Whens and Hows

Account service test

test/testAccountService.js:

describe('AccountService', () => {

let db = null;

let accountService = null;

beforeEach(() => {

db = {

querySync: sinon.stub()

};

accountService = new AccountService(db);

});

Page 51: Unit Testing - The Whys, Whens and Hows

Account service test (continued)

db.querySync.withArgs('account', { id: 1 }).returns([{

id: 1,

name: 'John Doe'

}]);

const callback = sinon.spy();

accountService.findById(1, callback);

expect(callback).to.have.been.calledWith({

id: 1,

name: 'John Doe'

});

Page 52: Unit Testing - The Whys, Whens and Hows

Promises

Page 53: Unit Testing - The Whys, Whens and Hows

DB now uses promises

src/accountService.js:

function AccountService(db) {

this.db = db;

}

AccountService.prototype.findById = function (accountId, callback) {

return this.db

.query('account', { id: accountId })

.then((results) => results[0]);

};

module.exports = AccountService;

Page 54: Unit Testing - The Whys, Whens and Hows

Bring in sinon-as-promised

https://www.npmjs.com/package/sinon-as-promised

npm install --save-dev sinon-as-promised

const sinonAsPromised = require('sinon-as-promised');

Page 55: Unit Testing - The Whys, Whens and Hows

Updated account service test

beforeEach(() => {

db = {

query: sinon.stub()

};

accountService = new AccountService(db);

});

Page 56: Unit Testing - The Whys, Whens and Hows

Updated account service test (continued)

db.query.withArgs('account', { id: 1 }).resolves([{

id: 1,

name: 'John Doe'

}]);

return accountService.findById(1)

.then((account) => {

expect(account).to.deep.equal({

id: 1,

name: 'John Doe'

});

});

Page 57: Unit Testing - The Whys, Whens and Hows

Negative case

Page 58: Unit Testing - The Whys, Whens and Hows

When account not found

db.query.withArgs('account', { id: -1 }).rejects(

new Error('Account not found')

);

return accountService.findById(-1)

.catch((error) => {

expect(error).to.deep.equal(

new Error('Account not found')

);

});

Page 59: Unit Testing - The Whys, Whens and Hows

But wait...

src/accountService.js:

AccountService.prototype.findById = function (accountId, callback) {

if (accountId === -1) {

return Promise.resolve({

id: -1,

name: 'Negative One'

});

}

return this.db

.query('account', { id: accountId })

.then((results) => results[0]);

};

Page 60: Unit Testing - The Whys, Whens and Hows

Run

npm test -- --grep account

AccountService

.findById()

when called for an existing account

✓ should return a promise resolved with the account

when called for a non-existent account

✓ should return a promise rejected with an error

Page 61: Unit Testing - The Whys, Whens and Hows

Need the positive case to fail the test

return accountService.findById(-1)

.catch((error) => {

expect(error).to.deep.equal(

new Error('Account not found')

);

})

.then(() => {

throw new Error('Should not have been resolved');

});

Page 62: Unit Testing - The Whys, Whens and Hows

Run

npm test -- --grep account

AccountService

.findById()

when called for an existing account

✓ should return a promise resolved with the account

when called for a non-existent account

1) should return a promise rejected with an error

Page 63: Unit Testing - The Whys, Whens and Hows

Making the experience better

Page 64: Unit Testing - The Whys, Whens and Hows

Bring in Chai as Promised

http://chaijs.com/plugins/chai-as-promised/

npm install --save-dev chai-as-promised

const chaiAsPromised = require('chai-as-promised');

chai.use(chaiAsPromised);

Page 65: Unit Testing - The Whys, Whens and Hows

Updated positive test

return expect(accountService.findById(1))

.to.eventually.deep.equal({

id: 1,

name: 'John Doe'

});

Page 66: Unit Testing - The Whys, Whens and Hows

Updated negative test

return expect(accountService.findById(-1))

.to.eventually.be.rejectedWith(Error, 'Account not found');

Page 67: Unit Testing - The Whys, Whens and Hows

Run

npm test -- --grep account

AccountService

.findById()

when called for an existing account

✓ should return a promise resolved with the account

when called for a non-existent account

1) should return a promise rejected with an error

AssertionError:

expected promise to be rejected with 'Error'

but it was fulfilled with { id: -1, name: 'Negative One' }

Page 68: Unit Testing - The Whys, Whens and Hows

Without dependency injection

Page 69: Unit Testing - The Whys, Whens and Hows

To intercept any module dependency - Mockery

https://github.com/mfncooper/mockery

npm install --save-dev mockery

beforeEach(() => {

mockery.enable({

warnOnReplace: false,

warnOnUnregistered: false,

useCleanCache: true

});

mockery.registerMock('./db', db);

});

afterEach(() => {

mockery.disable();

});

Page 70: Unit Testing - The Whys, Whens and Hows

All code so far

https://github.com/atesgoral/hello-test

Clean commit history with 1 commit per example.

Page 71: Unit Testing - The Whys, Whens and Hows

Q&A