Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unit Testing - The Whys, Whens and Hows

Ates Goral
October 11, 2016

Unit Testing - The Whys, Whens and Hows

Ates Goral

October 11, 2016
Tweet

More Decks by Ates Goral

Other Decks in Programming

Transcript

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

    View full-size slide

  2. Ates Goral
    @atesgoral
    http://magnetiq.com
    http://github.com/atesgoral
    http://stackoverflow.com/users/23501/ates-goral

    View full-size slide

  3. http://myplanet.com

    View full-size slide

  4. Definition of a unit test

    View full-size slide

  5. 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

    View full-size slide

  6. Inconsistent definitions
    Here’s what’s common:
    ● Written by developers
    ● Runs fast
    ● Deterministic
    ● Does not tread into integration test territory

    View full-size slide

  7. Appreciation of unit testing

    View full-size slide

  8. 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.

    View full-size slide

  9. Good unit tests

    View full-size slide

  10. 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.

    View full-size slide

  11. 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

    View full-size slide

  12. Good reasons

    View full-size slide

  13. 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).

    View full-size slide

  14. Benefits of unit testing

    View full-size slide

  15. 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

    View full-size slide

  16. Test-environment-first Programming

    View full-size slide

  17. 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.

    View full-size slide

  18. 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.

    View full-size slide

  19. Basic test environment setup

    View full-size slide

  20. 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)

    View full-size slide

  21. 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

    View full-size slide

  22. Let’s write our first proper test

    View full-size slide

  23. 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);
    });
    });
    });
    });

    View full-size slide

  24. Implementation and run
    src/arithmetic.js:
    *** REDACTED ***
    npm test
    arithmetic
    .sum()
    when called with two numbers
    ✓ should return their sum

    View full-size slide

  25. Opportunistic implementation
    src/arithmetic.js:
    exports.sum = (a, b) => {
    return 4;
    };

    View full-size slide

  26. https://xkcd.com/221/

    View full-size slide

  27. Who tests the tests?

    View full-size slide

  28. 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.

    View full-size slide

  29. Selectively running tests with Mocha
    mocha --grep
    npm test -- --grep
    e.g.
    npm test -- --grep arithmetic

    View full-size slide

  30. Let’s get asynchronous

    View full-size slide

  31. Timeout implementation
    src/timeout.js:
    exports.set = (callback, milliseconds) => {
    setTimeout(callback, milliseconds);
    };

    View full-size slide

  32. 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);
    });

    View full-size slide

  33. 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

    View full-size slide

  34. Flaky tests are evil

    View full-size slide

  35. 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

    View full-size slide

  36. Deterministic timing

    View full-size slide

  37. Bring in Sinon
    http://sinonjs.org/
    npm install --save-dev sinon

    View full-size slide

  38. 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();
    });

    View full-size slide

  39. 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;
    });
    });
    });

    View full-size slide

  40. 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.

    View full-size slide

  41. Definitions of test doubles

    View full-size slide

  42. Again, some inconsistencies
    ● Dummy
    ● Fake
    ● Stub
    ● Spy
    ● Mock
    http://www.martinfowler.com/bliki/TestDouble.html
    https://en.wikipedia.org/wiki/Test_double

    View full-size slide

  43. Test doubles - dependency injection

    View full-size slide

  44. 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;

    View full-size slide

  45. Bring in Sinon-Chai
    https://github.com/domenic/sinon-chai
    npm install --save-dev sinon-chai
    const sinonChai = require('sinon-chai');
    chai.use(sinonChai);

    View full-size slide

  46. Account service test
    test/testAccountService.js:
    describe('AccountService', () => {
    let db = null;
    let accountService = null;
    beforeEach(() => {
    db = {
    querySync: sinon.stub()
    };
    accountService = new AccountService(db);
    });

    View full-size slide

  47. 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'
    });

    View full-size slide

  48. 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;

    View full-size slide

  49. 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');

    View full-size slide

  50. Updated account service test
    beforeEach(() => {
    db = {
    query: sinon.stub()
    };
    accountService = new AccountService(db);
    });

    View full-size slide

  51. 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'
    });
    });

    View full-size slide

  52. Negative case

    View full-size slide

  53. 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')
    );
    });

    View full-size slide

  54. 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]);
    };

    View full-size slide

  55. 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

    View full-size slide

  56. 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');
    });

    View full-size slide

  57. 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

    View full-size slide

  58. Making the experience better

    View full-size slide

  59. 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);

    View full-size slide

  60. Updated positive test
    return expect(accountService.findById(1))
    .to.eventually.deep.equal({
    id: 1,
    name: 'John Doe'
    });

    View full-size slide

  61. Updated negative test
    return expect(accountService.findById(-1))
    .to.eventually.be.rejectedWith(Error, 'Account not found');

    View full-size slide

  62. 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' }

    View full-size slide

  63. Without dependency injection

    View full-size slide

  64. 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();
    });

    View full-size slide

  65. All code so far
    https://github.com/atesgoral/hello-test
    Clean commit history with 1 commit per example.

    View full-size slide