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

Testing JavaScript

Testing JavaScript

My presentation from js.next();

Self-testing code is essential for producing quality software. Unfortunately, a lot of people think testing in JavaScript is too hard. But a lot of people thought it was a toy language too.

In the presentation I talked about how to write good tests for JavaScript and shared a lot of tips and tricks.

Radoslav Stankov

November 23, 2014
Tweet

More Decks by Radoslav Stankov

Other Decks in Technology

Transcript

  1. Nov 23,
    Nov 23, 2014
    Sofia
    var title = “Testing JavaScript”;
    var info = {
    name: “Radoslav Stankov”,
    twitter: “@rstankov”
    };

    View Slide

  2. Nov 23,
    Stuff I’m not going to
    talk about

    View Slide

  3. Nov 23,
    Reasons for automated testings

    View Slide

  4. Nov 23,
    Test driven development

    View Slide

  5. Nov 23,
    Test-induced design damage

    View Slide

  6. Nov 23,
    Philosophy surrounding testing

    View Slide

  7. Nov 23,
    Selling testing

    View Slide

  8. Nov 23,
    I’m going to about

    View Slide

  9. Nov 23,

    View Slide

  10. Nov 23,
    Hard to find things

    View Slide

  11. Nov 23,
    Who am I?

    View Slide

  12. Nov 23,
    Radoslav Stankov
    @rstankov
    http://github.com/rstankov
    http://rstankov.com

    View Slide

  13. Nov 23,

    View Slide

  14. Nov 23,
    My “history”

    View Slide

  15. Nov 23,
    • 2002 - first “professional" project (Flash)
    My “history”

    View Slide

  16. Nov 23,
    • 2002 - first “professional" project (Flash)
    • 2003 - first JavaScript “production” code
    My “history”

    View Slide

  17. Nov 23,
    • 2002 - first “professional" project (Flash)
    • 2003 - first JavaScript “production” code
    • 2008 - first PHP unit test
    My “history”

    View Slide

  18. Nov 23,
    • 2002 - first “professional" project (Flash)
    • 2003 - first JavaScript “production” code
    • 2008 - first PHP unit test
    • 2009 - first Ruby unit test
    My “history”

    View Slide

  19. Nov 23,
    • 2002 - first “professional" project (Flash)
    • 2003 - first JavaScript “production” code
    • 2008 - first PHP unit test
    • 2009 - first Ruby unit test
    • 2009 - first JavaScript unit test
    My “history”

    View Slide

  20. Nov 23,
    • 2002 - first “professional" project (Flash)
    • 2003 - first JavaScript “production” code
    • 2008 - first PHP unit test
    • 2009 - first Ruby unit test
    • 2009 - first JavaScript unit test
    • 2010 - first selenium integration test
    My “history”

    View Slide

  21. Nov 23,
    • 2002 - first “professional" project (Flash)
    • 2003 - first JavaScript “production” code
    • 2008 - first PHP unit test
    • 2009 - first Ruby unit test
    • 2009 - first JavaScript unit test
    • 2010 - first selenium integration test
    • 2011 - heavy JavaScript unit testing
    My “history”

    View Slide

  22. Nov 23,
    • 2002 - first “professional" project (Flash)
    • 2003 - first JavaScript “production” code
    • 2008 - first PHP unit test
    • 2009 - first Ruby unit test
    • 2009 - first JavaScript unit test
    • 2010 - first selenium integration test
    • 2011 - heavy JavaScript unit testing
    • 2014 - I’m still here :P
    My “history”

    View Slide

  23. Nov 23,

    View Slide

  24. Nov 23,
    What is a self-testing
    code?

    View Slide

  25. Nov 23,
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  26. Nov 23,
    Test Suite suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  27. Nov 23,
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  28. Nov 23,
    Test Setup
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  29. Nov 23,
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  30. Nov 23,
    Test Case
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  31. Nov 23,
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  32. Nov 23,
    Assertion
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  33. Nov 23,
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  34. Nov 23,

    View Slide

  35. Nov 23,

    View Slide

  36. Nov 23,
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  37. Nov 23,
    xUnit
    Kent Beck
    based on SUnit (Smalltalk)
    designed around 1998

    View Slide

  38. Nov 23,
    suite("Calculator", function() {
    setup(function() {
    this.calculator = new Calculator(2)
    });
    test("adding a value", function() {
    this.calculator.add(2);
    assert(this.calculator.value == 4);
    });
    test("multiply a value", function() {
    this.calculator.multiply(3);
    assert(this.calculator.value == 6);
    });
    });

    View Slide

  39. Nov 23,
    describe("Calculator", function() {
    beforeEach(function() {
    this.calculator = new Calculator(2)
    });
    it("can add a value", function() {
    this.calculator.add(2);
    expect(this.calculator.value).to.eq(4);
    });
    it("can multiply a value", function() {
    this.calculator.multiply(3);
    expect(this.calculator.value).to.eq(6);
    });
    });

    View Slide

  40. Nov 23,
    describe("Calculator", function() {
    beforeEach(function() {
    this.calculator = new Calculator(2)
    });
    it("can add a value", function() {
    this.calculator.add(2);
    expect(this.calculator.value).to.eq(4);
    });
    it("can multiply a value", function() {
    this.calculator.multiply(3);
    expect(this.calculator.value).to.eq(6);
    });
    });
    Example
    Group

    View Slide

  41. Nov 23,
    describe("Calculator", function() {
    beforeEach(function() {
    this.calculator = new Calculator(2)
    });
    it("can add a value", function() {
    this.calculator.add(2);
    expect(this.calculator.value).to.eq(4);
    });
    it("can multiply a value", function() {
    this.calculator.multiply(3);
    expect(this.calculator.value).to.eq(6);
    });
    });

    View Slide

  42. Nov 23,
    describe("Calculator", function() {
    beforeEach(function() {
    this.calculator = new Calculator(2)
    });
    it("can add a value", function() {
    this.calculator.add(2);
    expect(this.calculator.value).to.eq(4);
    });
    it("can multiply a value", function() {
    this.calculator.multiply(3);
    expect(this.calculator.value).to.eq(6);
    });
    });
    Setup

    View Slide

  43. Nov 23,
    describe("Calculator", function() {
    beforeEach(function() {
    this.calculator = new Calculator(2)
    });
    it("can add a value", function() {
    this.calculator.add(2);
    expect(this.calculator.value).to.eq(4);
    });
    it("can multiply a value", function() {
    this.calculator.multiply(3);
    expect(this.calculator.value).to.eq(6);
    });
    });

    View Slide

  44. Nov 23,
    describe("Calculator", function() {
    beforeEach(function() {
    this.calculator = new Calculator(2)
    });
    it("can add a value", function() {
    this.calculator.add(2);
    expect(this.calculator.value).to.eq(4);
    });
    it("can multiply a value", function() {
    this.calculator.multiply(3);
    expect(this.calculator.value).to.eq(6);
    });
    });
    Example

    View Slide

  45. Nov 23,
    describe("Calculator", function() {
    beforeEach(function() {
    this.calculator = new Calculator(2)
    });
    it("can add a value", function() {
    this.calculator.add(2);
    expect(this.calculator.value).to.eq(4);
    });
    it("can multiply a value", function() {
    this.calculator.multiply(3);
    expect(this.calculator.value).to.eq(6);
    });
    });

    View Slide

  46. Nov 23,
    describe("Calculator", function() {
    beforeEach(function() {
    this.calculator = new Calculator(2)
    });
    it("can add a value", function() {
    this.calculator.add(2);
    expect(this.calculator.value).to.eq(4);
    });
    it("can multiply a value", function() {
    this.calculator.multiply(3);
    expect(this.calculator.value).to.eq(6);
    });
    });
    Expectation

    View Slide

  47. Nov 23,
    describe("Calculator", function() {
    beforeEach(function() {
    this.calculator = new Calculator(2)
    });
    it("can add a value", function() {
    this.calculator.add(2);
    expect(this.calculator.value).to.eq(4);
    });
    it("can multiply a value", function() {
    this.calculator.multiply(3);
    expect(this.calculator.value).to.eq(6);
    });
    });

    View Slide

  48. Nov 23,
    Rspec
    Dave Astels
    David Chelimsky

    inspired by JBehave in 2008

    View Slide

  49. Nov 23,
    Unit
    Test
    Integration
    Test
    Acceptance
    Test
    Tests one object Tests one “component” Tests the whole stack
    Isolated with mocks Some times uses stubs
    Uses the UI

    Expresses domain terms
    Fast Slow Very slow
    Helps for good design Helps for verification Helps for verification
    Main test types

    View Slide

  50. Nov 23,

    View Slide

  51. Nov 23,
    $(function() {
    $('#search-form').on('submit', function(e) {
    e.preventDefault();
    var query = $('#search-input').val().trim();
    if (!query.length) { return; }
    if ($('#search-results').hasClass('loading')) { return; }
    $('#search-results').empty()
    $('#search-results').addClass('loading');
    $.getJSON('/search?q=' + query, function(results) {
    $('#search-results').removeClass('loading');
    results.forEach(function(item) {
    $('#search-results').append('' + item.name + '');
    });
    });
    });
    });

    View Slide

  52. Nov 23,
    app.initSearch = function() {
    $('#search-form').on('submit', function(e) {
    e.preventDefault();
    var query = $('#search-input').val().trim();
    if (!query.length) { return; }
    if ($('#search-results').hasClass('loading')) { return; }
    $('#search-results').empty()
    $('#search-results').addClass('loading');
    $.getJSON('/search?q=' + query, function(results) {
    $('#search-results').removeClass('loading');
    results.forEach(function(item) {
    $('#search-results').append('' + item.name + '');
    });
    });
    });
    };

    View Slide

  53. Nov 23,
    test("searching", function() {
    var form = $('').appendTo('body');
    var callback;
    var calls = 0;
    $.getJSON = function(url, success) {
    assert(url === '/search?q=value');
    callback = success;
    calls += 1 ;
    }
    app.initSearch();
    var input = $('').appendTo(form);
    assert(input.val().length == 0);
    input.val('');
    form.submit();
    assert(calls == 0);
    input.val('value');
    var results = $('').appendTo(form);
    form.submit();
    assert(results.hasClass('loading'));
    form.submit();
    form.submit();
    assert(calls == 1);
    callback([
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}
    ]);
    assert(!results.hasClass('loading'));
    assert(results.find('a').length == 2);
    assert(results.find('a[href="/products/1"]').length == 1);
    assert(results.find('a[href="/products/2"]').length == 1);
    });

    View Slide

  54. Nov 23,
    test("searching", function() {
    var form = $('').appendTo('body');
    var callback;
    var calls = 0;
    $.getJSON = function(url, success) {
    assert(url === '/search?q=value');
    callback = success;
    calls += 1 ;
    }
    app.initSearch();
    var input = $('').appendTo(f
    assert(input.val().length == 0);
    input.val('');
    form.submit();
    assert(calls == 0);
    input.val('value');

    View Slide

  55. Nov 23,
    app.initSearch();
    var input = $('').appendTo(f
    assert(input.val().length == 0);
    input.val('');
    form.submit();
    assert(calls == 0);
    input.val('value');
    var results = $('').appendTo(form);
    form.submit();
    assert(results.hasClass('loading'));
    form.submit();
    form.submit();
    assert(calls == 1);
    callback([

    View Slide

  56. Nov 23,
    var results = $('').appendTo(form);
    form.submit();
    assert(results.hasClass('loading'));
    form.submit();
    form.submit();
    assert(calls == 1);
    callback([
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}
    ]);
    assert(!results.hasClass('loading'));
    assert(results.find('a').length == 2);
    assert(results.find('a[href="/products/1"]').length == 1);
    assert(results.find('a[href="/products/2"]').length == 1);
    });

    View Slide

  57. Nov 23,

    View Slide

  58. Nov 23,
    What is a good test?

    View Slide

  59. Nov 23,
    Easy to run
    “I don’t have to think how to run my
    tests.”

    View Slide

  60. Nov 23,
    Quick
    “Running tests, should not distract
    me from the problem I’m solving.”

    View Slide

  61. Nov 23,
    Simple
    “I should not spend any energy if
    understanding the test.”

    View Slide

  62. Nov 23,
    Reliable
    “I should have confidence, that
    when my test pass, my code is most
    probably is working.”

    View Slide

  63. Nov 23,
    Flexible
    “I should not rewrite my tests, every
    time I change something.”

    View Slide

  64. Nov 23,
    Localisable
    “When a test fails I should know
    exactly why?”

    View Slide

  65. Nov 23,
    Isolated
    “One test should not depend on
    other test”

    View Slide

  66. Nov 23,
    • Easy to run
    • Quick
    • Simple
    • Reliable
    • Flexible
    • Localisable
    • Isolated

    View Slide

  67. Nov 23,
    test("searching", function() {
    var form = $('').appendTo('body');
    var callback;
    var calls = 0;
    $.getJSON = function(url, success) {
    assert(url === '/search?q=value');
    callback = success;
    calls += 1 ;
    }
    app.initSearch();
    var input = $('').appendTo(form);
    assert(input.val().length == 0);
    input.val('');
    form.submit();
    assert(calls == 0);
    input.val('value');
    var results = $('').appendTo(form);
    form.submit();
    assert(results.hasClass('loading'));
    form.submit();
    form.submit();
    assert(calls == 1);
    callback([
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}
    ]);
    assert(!results.hasClass('loading'));
    assert(results.find('a').length == 2);
    assert(results.find('a[href="/products/1"]').length == 1);
    assert(results.find('a[href="/products/1"]').length == 1);
    });

    View Slide

  68. Nov 23,
    • Easy to run
    • Quick
    • Simple
    • Reliable
    • Flexible
    • Localisable
    • Isolated

    View Slide

  69. Nov 23,
    4 Phase testing

    View Slide

  70. Nov 23,

    View Slide

  71. Nov 23,
    4 Phase testing

    View Slide

  72. Nov 23,
    Setup
    4 Phase testing

    View Slide

  73. Nov 23,
    Setup
    Action
    4 Phase testing

    View Slide

  74. Nov 23,
    Setup
    Action
    Assertion
    4 Phase testing

    View Slide

  75. Nov 23,
    Setup
    Action
    Assertion
    Teardown
    4 Phase testing

    View Slide

  76. Nov 23,
    Setup
    Action
    Assertion
    Teardown
    4 Phase testing

    View Slide

  77. Nov 23,
    test("searching", function() {
    var form = $('').appendTo('body');
    var callback;
    var calls = 0;
    $.getJSON = function(url, success) {
    assert(url === '/search?q=value');
    callback = success;
    calls += 1 ;
    }
    app.initSearch();
    var input = $('').appendTo(form);
    assert(input.val().length == 0);
    input.val('');
    form.submit();
    assert(calls == 0);
    input.val('value');
    var results = $('').appendTo(form);
    form.submit();
    assert(results.hasClass('loading'));
    form.submit();
    form.submit();
    assert(calls == 1);
    callback([
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}
    ]);
    assert(!results.hasClass('loading'));
    assert(results.find('a').length == 2);
    assert(results.find('a[href="/products/1"]').length == 1);
    assert(results.find('a[href="/products/2"]').length == 1);
    });

    View Slide

  78. Nov 23,
    test("searching", function() {
    var form = $('').appendTo('body');
    var callback;
    var calls = 0;
    $.getJSON = function(url, success) {
    assert(url === '/search?q=value');
    callback = success;
    calls += 1 ;
    }
    app.initSearch();
    var input = $('').appendTo(form);
    assert(input.val().length == 0);
    input.val('');
    form.submit();
    assert(calls == 0);
    input.val('value');
    var results = $('').appendTo(form);
    form.submit();
    assert(results.hasClass('loading'));
    form.submit();
    form.submit();
    assert(calls == 1);
    callback([
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}
    ]);
    assert(!results.hasClass('loading'));
    assert(results.find('a').length == 2);
    assert(results.find('a[href="/products/1"]').length == 1);
    assert(results.find('a[href="/products/2"]').length == 1);
    });

    View Slide

  79. Nov 23,

    View Slide

  80. Nov 23,

    View Slide

  81. Nov 23,
    suite("searching", function() {
    var form, input, results, server;
    setup(function() {
    form = $('').appendTo('body');
    results = $('').appendTo(form);
    input = $('').appendTo(form);
    input.val('query');
    var data = [
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}
    ]
    server = sinon.fakeServer.create();
    server.respondWith("GET", "/search?q=query", JSON.stringify(data)
    app.initSearch();
    });
    teardown(function() {
    form.remove();

    View Slide

  82. Nov 23,
    app.initSearch();
    });
    teardown(function() {
    form.remove();
    server.restore();
    });
    test("empty search", function() {
    input.val('');
    form.submit();
    assert(server.requests.length == 0);
    });
    test("double submit", function() {
    form.submit();
    form.submit();
    assert(server.requests.length == 1);
    });
    test("adds loading class during search", function() {

    View Slide

  83. Nov 23,
    assert(server.requests.length == 1);
    });
    test("adds loading class during search", function() {
    form.submit();
    assert(results.hasClass('loading'));
    });
    test("removes loading class after search", function() {
    form.submit();
    server.respond();
    assert(!results.hasClass('loading'));
    });
    test("renders results", function() {
    form.submit();
    server.respond();
    assert(results.find('a').length == 2);

    View Slide

  84. Nov 23,
    form.submit();
    assert(results.hasClass('loading'));
    });
    test("removes loading class after search", function() {
    form.submit();
    server.respond();
    assert(!results.hasClass('loading'));
    });
    test("renders results", function() {
    form.submit();
    server.respond();
    assert(results.find('a').length == 2);
    assert(results.find('a[href="/products/1"]').length == 1);
    assert(results.find('a[href="/products/2"]').length == 1);
    });
    });

    View Slide

  85. Nov 23,

    View Slide

  86. Nov 23,
    suite("searching", function() {
    var form, input, results, server;
    setup(function() {
    form = $('').appendTo('body');
    results = $('').appendTo(form);
    input = $('').appendTo(form);
    input.val('query');
    var data = [
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}
    ]
    server = sinon.fakeServer.create();
    server.respondWith("GET", "/search?q=query", JSON.stringify(data));
    app.initSearch();
    });
    teardown(function() {
    form.remove();
    server.restore();
    });
    test("empty search", function() {
    input.val('');
    form.submit();
    assert(server.requests.length == 0);
    });
    test("double submit", function() {
    form.submit();
    form.submit();
    assert(server.requests.length == 1);
    });
    test("adds loading class during search", function() {
    form.submit();
    assert(results.hasClass('loading'));
    });
    test("removes loading class after search", function() {
    form.submit();
    server.respond();
    assert(!results.hasClass('loading'));
    });
    test("renders results", function() {
    form.submit();
    server.respond();
    assert(results.find('a').length == 2);
    assert(results.find('a[href="/products/1"]').length == 1);
    assert(results.find('a[href="/products/1"]').length == 1);
    });
    });

    View Slide

  87. Nov 23,
    test("empty search", function() {
    input.val('');
    form.submit();
    assert(server.requests.length == 0);
    });

    View Slide

  88. Nov 23,
    test("empty search", function() {
    input.val('');
    form.submit();
    assert.equal(0, server.requests.length);
    });

    View Slide

  89. Nov 23,

    View Slide

  90. Nov 23,

    View Slide

  91. Nov 23,
    suite("searching", function() {
    setup(function() { /* . . . */ });
    teardown(function() { /* . . . */ });
    test("empty search", function() {
    // . . .
    assert.equal(0, server.requests.length);
    });
    test("double submit", function() {
    // . . .
    assert.equal(1, server.requests.length == 1);
    });
    test("adds loading class during search", function() {
    // . . .
    assert.ok(results.hasClass(‘loading'));
    });
    test("removes loading class after search", function() {
    // . . .
    assert.ok(!results.hasClass(‘loading'));
    });
    test("renders results", function() {
    // . . .
    assert(results.find('a[href="/products/1"]').length == 1);
    assert(results.find('a[href="/products/1"]').length == 1);
    });
    });

    View Slide

  92. Nov 23,
    describe("Search", function() {
    beforeEach(function() { /* . . . */ });
    afterEach(function() { /* . . . */ });
    it("doesn't search on empty query", function() {
    // . . .
    expect(server.requests.length).to.eq(0);
    });
    it("guards against double submit", function() {
    // . . .
    expect(server.requests.length).to.eq(1);
    });
    it("adds loading class during search", function() {
    // . . .
    expect(results).to.have.class('loading');
    });
    it("removes loading class after search", function() {
    // . . .
    expect(results).not.to.have.class('loading');
    });
    it("renders results", function() {
    // . . .
    expect(results).to.have.descendants('a[href="/products/1"]')
    expect(results).to.have.descendants('a[href="/products/2"]')
    });
    });

    View Slide

  93. Nov 23,
    Mocks

    View Slide

  94. Nov 23,
    Mock Objects: friends of foes?

    View Slide

  95. Nov 23,
    Fake
    Used as a simpler implementation, e.g. using
    an in-memory database in the tests instead
    of doing real database access.

    View Slide

  96. Nov 23,
    Dummy object
    Used when a parameter is needed for the
    tested method but without actually needing
    to use the parameter.

    View Slide

  97. Nov 23,
    Stub
    Test stubs are objects with pre-programmed
    behaviour.

    View Slide

  98. Nov 23,
    Spy
    Records arguments, return value, the value
    of this and exception thrown (if any) for all
    its calls.

    View Slide

  99. Nov 23,
    Mock
    Mocks are fake methods (like spies) with pre-
    programmed behaviour (like stubs) as well as
    pre-programmed expectations.
    A mock will fail your test if it is not used as
    expected.

    View Slide

  100. Nov 23,
    Test double
    Stub
    Spy
    Mock
    Fake
    Dummy

    View Slide

  101. Nov 23,
    http://sinonjs.org/docs/

    View Slide

  102. Nov 23,
    Fragile tests

    View Slide

  103. Nov 23,
    Bad fixtures
    suite("searching", function() {
    var form, input, results;
    setup(function() {
    form = $('').appendTo('body')
    results = $('').appendTo(form);
    input = $('').appendTo(form);
    input.val('query');
    app.initSearch();
    });
    /*
    teardown(function() {
    form.remove();
    });
    */
    });

    View Slide

  104. Nov 23,
    Bad fixtures
    suite("searching", function() {
    var form, input, results;
    setup(function() {
    form = $('').appendTo('body')
    results = $('').appendTo(form);
    input = $('').appendTo(form);
    input.val('query');
    app.initSearch();
    });
    teardown(function() {
    form.remove();
    });
    });

    View Slide

  105. Nov 23,
    Bad fixtures
    var input = $('').appendTo(form);
    assert(input.val().length == 0);

    View Slide

  106. Nov 23,
    Too much information
    describe("Task", function() {
    describe("isCompleted", function() {
    it("returns true when state is 'completed'", function() {
    var task = new Task('Buy milk', 'completed');
    expect(task.isCompleted()).to.be.eq(true)
    });
    it("returns false when state is 'opened'", function() {
    var task = new Task('Buy milk', 'opened');
    expect(task.isCompleted()).to.be.eq(false)
    });
    });
    });

    View Slide

  107. Nov 23,
    Too much information
    describe("Task", function() {
    describe("isCompleted", function() {
    it("returns true when state is 'completed'", function() {
    var task = newTaskWithState('completed');
    expect(task.isCompleted()).to.be.eq(true)
    });
    it("returns false when state is 'opened'", function() {
    var task = newTaskWithState('opened');
    expect(task.isCompleted()).to.be.eq(false)
    });
    });
    });

    View Slide

  108. Nov 23,
    Indirection
    describe("Task.completeAll", function() {
    it("completes a task by text mask", function() {
    var task = new Task(text: 'Create slides');
    var other = new Task(text: 'Create examples');
    var collection = new OngoingTasksCollection([task, other]);
    Task.completeAll([task, other], text: 'slides');
    expect(collection.length).to.eq(1);
    expect(collection.at(0)).to.eq(other);
    });
    });

    View Slide

  109. Nov 23,
    Indirection
    describe("Task.completeAll", function() {
    it("completes tasks who match query", function() {
    var task = new Task(text: 'Create slides');
    Task.completeAll([task], text: 'slides');
    expect(task.isCompleted()).to.eq(true)
    });
    it("doesn't complete tasks who don't match query", function() {
    var task = new Task(text: 'Create examples');
    Task.completeAll([task], text: 'slides');
    expect(task.isCompleted()).to.eq(false)
    });
    });

    View Slide

  110. Nov 23,
    Indirection
    describe("OngoingTasksCollection", function() {
    it("removes a task when completed", function() {
    var task = new Task();
    var collection = new OngoingTasksCollection([task]);
    task.complete();
    expect(collection.length).to.eq(0);
    });
    });

    View Slide

  111. Nov 23,
    Coupling
    describe("Wallet.buy", function() {
    it("buys a product", function("something") {
    var amount = null;
    var fakeGatway = {
    isActive: function(_) { return true; },
    processor: function() {
    return {
    haveEnoughMoney: function(_) { return true; },
    process: function(product) { amount = product.price; }
    };
    }
    };
    new Wallet(fakeGatway).buy({amount: 5});
    expect(amount).to.eq(5);
    });
    });

    View Slide

  112. Nov 23,
    describe("Wallet.buy", function() {
    it("buys a product", function("something") {
    var process = sinon.spy();
    new Wallet(process).buy({amount: 5});
    expect(process).to.have.been.calledWith(5);
    });
    });
    ✔ Coupling

    View Slide

  113. Nov 23,
    Time related
    describe("timeago", function() {
    // . . .
    it("returns 1 day ago if date is yesterday", function() {
    deleteButton.click(); // sliding effect
    expect(productDiv).to.not.be.visible;
    });
    });

    View Slide

  114. Nov 23,
    Time related
    describe("timeago", function() {
    // . . .
    it("returns 1 day ago if date is yesterday", function(done) {
    deleteButton.click(); // sliding effect
    setTimeout(function() {
    expect(productDiv).to.not.be.visible;
    }, 5000);
    });
    });

    View Slide

  115. Nov 23,
    Time related
    describe("timeago", function() {
    // . . .
    beforeEach(function() {
    jQuery.fx.off = true;
    });
    afterEach(function() {
    jQuery.fx.off = false;
    });
    it("returns 1 day ago if date is yesterday", function() {
    deleteButton.click(); // sliding effect
    expect(productDiv).to.not.be.visible;
    });
    });

    View Slide

  116. Nov 23,
    JavaScript tips

    View Slide

  117. Nov 23,
    AJAX

    View Slide

  118. Nov 23,
    var callback;
    var calls = 0;
    $.getJSON = function(url, success) {
    assert(url === '/search?q=value');
    callback = success;
    calls += 1 ;
    }

    View Slide

  119. Nov 23,
    var callback;
    var calls = 0;
    $.getJSON = function(url, success) {
    assert(url === '/search?q=value');
    callback = success;
    calls += 1 ;
    }

    View Slide

  120. Nov 23,
    var callback;
    var calls = 0;
    $.getJSON = function(url, success) {
    assert(url === '/search?q=value');
    callback = success;
    calls += 1 ;
    }

    View Slide

  121. Nov 23,
    var data = [
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}
    ]
    server = sinon.fakeServer.create();
    server.respondWith("GET", "/search?q=query", JSON.stringify(data));

    View Slide

  122. Nov 23,
    app.initSearch = function() {
    $('#search-form').on('submit', function(e) {
    e.preventDefault();
    var query = $('#search-input').val().trim();
    if (!query.length) { return; }
    if ($('#search-results').hasClass('loading')) { return; }
    $('#search-results').empty()
    $('#search-results').addClass('loading');
    $.getJSON('/search?q=' + query, function(results) {
    $('#search-results').removeClass('loading');
    results.forEach(function(item) {
    $('#search-results').append('

    View Slide

  123. Nov 23,
    app.initSearch = function(search) {
    search || (search = new app.Search());
    search.on('query:new', function() {
    $('#search-results').addClass('loading').empty();
    });
    search.on('query:results', function(results) {
    $('#search-results').removeClass('loading');
    results.forEach(function(item) {
    $('#search-results').append('
    });
    });
    $('#search-form').on('submit', function(e) {
    e.preventDefault();
    search.newQuery($('#search-input').val().trim());
    });
    };

    View Slide

  124. Nov 23,
    Async
    var delay = function(callback) {
    setTimeout(function() {
    callback('wait for it...');
    }, 1000);
    };
    it('is called later', function(done) {
    delay(function(message) {
    expect(message).to.eq('wait for it...');
    done();
    });
    });

    View Slide

  125. Nov 23,
    app.initSearch = function(search) {
    search || (search = new app.Search());
    search.on('query:new', function() {
    $('#search-results').addClass('loading').empty();
    });
    search.on('query:results', function(results) {
    $('#search-results').removeClass('loading');
    results.forEach(function(item) {
    $('#search-results').append('
    });
    });
    $('#search-form').on('submit', function(e) {
    e.preventDefault();
    search.newQuery($('#search-input').val().trim());
    });
    };

    View Slide

  126. Nov 23,
    it("sets new query on submit", function() {
    input.val('query');
    form.submit();
    expect(search.query).to.eq('query');
    });
    it("adds loading class during search", function() {
    search.trigger('query:new');
    expect(results).to.have.class('loading');
    });
    it("removes loading class after search", function() {
    search.trigger('query:results', []);
    expect(results).not.to.have.class('loading');
    });
    it("renders results", function() {
    search.trigger('query:results', [
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}

    View Slide

  127. Nov 23,
    it("adds loading class during search", function() {
    search.trigger('query:new');
    expect(results).to.have.class('loading');
    });
    it("removes loading class after search", function() {
    search.trigger('query:results', []);
    expect(results).not.to.have.class('loading');
    });
    it("renders results", function() {
    search.trigger('query:results', [
    {id: 1, name: 'Phone'},
    {id: 2, name: 'Tablet'}
    ]);
    expect(results).to.have.descendants('a[href="/products/1"]')
    expect(results).to.have.descendants('a[href="/products/2"]')
    });

    View Slide

  128. Nov 23,
    DOM

    View Slide

  129. Nov 23,
    var browser = require("testium").getBrowser();
    describe("Search", function() {
    beforeEach(function(done) {
    new Product({name: 'Phone'}).save(done);
    });
    afterEach(function(done) {
    browser.navigateTo('/');
    browser.clearCookies();
    store.getConnection().flushdb(done);
    });
    it("filters products by name", function() {
    browser.navigateTo('/search');
    browser.type('[name=query]', 'Phone');
    browser.click('[type=submit]');
    browser.waitForElement('#search-results a');
    browser.assert.elementHasText('#search-results a', 'Phone');
    });
    });

    View Slide

  130. Nov 23,
    jQuery Spaghetti

    View Slide

  131. Nov 23,
    jQuery Spaghetti
    • Initial spaghetti search
    • Extracted initial ugly fragile test
    • Improved test by splitting it
    • Switch test style from TDD to BDD
    • Extracted Search model
    • Improved test by using FakeSearch

    View Slide

  132. Nov 23,
    Tools

    View Slide

  133. Nov 23,

    View Slide

  134. Nov 23,

    View Slide

  135. Nov 23,

    View Slide

  136. Nov 23,

    View Slide

  137. Nov 23,

    View Slide

  138. Nov 23,

    View Slide

  139. Nov 23,

    View Slide

  140. Nov 23,

    View Slide

  141. Nov 23,

    View Slide

  142. Nov 23,
    Books

    View Slide

  143. Nov 23,

    View Slide

  144. Nov 23,

    View Slide

  145. Nov 23,

    View Slide

  146. Nov 23,
    Summary

    View Slide

  147. Nov 23,
    • Test terminology
    • 4 Phase testing
    • Mocks
    • Fragile tests
    • Tips and Tricks
    • Tools and Books
    Summary

    View Slide

  148. Nov 23,
    https://speakerdeck.com/rstankov/testing-javascript
    Slides

    View Slide

  149. Nov 23,
    https://github.com/RStankov/talks-code
    Examples

    View Slide

  150. Nov 23,
    Thanks

    View Slide

  151. Nov 23,
    Questions?

    View Slide