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.

7a0e72a6f55811246bb5d9a946fd2e49?s=128

Radoslav Stankov

November 23, 2014
Tweet

Transcript

  1. Nov 23, Nov 23, 2014 Sofia var title = “Testing

    JavaScript”; var info = { name: “Radoslav Stankov”, twitter: “@rstankov” };
  2. Nov 23, Stuff I’m not going to talk about

  3. Nov 23, Reasons for automated testings

  4. Nov 23, Test driven development

  5. Nov 23, Test-induced design damage

  6. Nov 23, Philosophy surrounding testing

  7. Nov 23, Selling testing

  8. Nov 23, I’m going to about

  9. Nov 23,

  10. Nov 23, Hard to find things

  11. Nov 23, Who am I?

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

  13. Nov 23,

  14. Nov 23, My “history”

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

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

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

    2003 - first JavaScript “production” code • 2008 - first PHP unit test My “history”
  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”
  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”
  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”
  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”
  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”
  23. Nov 23,

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

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

  35. Nov 23,

  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); }); });
  37. Nov 23, xUnit Kent Beck based on SUnit (Smalltalk) designed

    around 1998
  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); }); });
  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); }); });
  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
  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); }); });
  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
  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); }); });
  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
  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); }); });
  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
  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); }); });
  48. Nov 23, Rspec Dave Astels David Chelimsky
 inspired by JBehave

    in 2008
  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
  50. Nov 23,

  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('<a href="/products/' + item.id + '">' + item.name + '</a>'); }); }); }); });
  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('<a href="/products/' + item.id + '">' + item.name + '</a>'); }); }); }); };
  53. Nov 23, test("searching", function() { var form = $('<form id="search-form"></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 = $('<input id="search-input" type="text" />').appendTo(form); assert(input.val().length == 0); input.val(''); form.submit(); assert(calls == 0); input.val('value'); var results = $('<div id="search-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); });
  54. Nov 23, test("searching", function() { var form = $('<form id="search-form"></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 = $('<input id="search-input" type="text" />').appendTo(f assert(input.val().length == 0); input.val(''); form.submit(); assert(calls == 0); input.val('value');
  55. Nov 23, app.initSearch(); var input = $('<input id="search-input" type="text" />').appendTo(f

    assert(input.val().length == 0); input.val(''); form.submit(); assert(calls == 0); input.val('value'); var results = $('<div id="search-results" />').appendTo(form); form.submit(); assert(results.hasClass('loading')); form.submit(); form.submit(); assert(calls == 1); callback([
  56. Nov 23, var results = $('<div id="search-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); });
  57. Nov 23,

  58. Nov 23, What is a good test?

  59. Nov 23, Easy to run “I don’t have to think

    how to run my tests.”
  60. Nov 23, Quick “Running tests, should not distract me from

    the problem I’m solving.”
  61. Nov 23, Simple “I should not spend any energy if

    understanding the test.”
  62. Nov 23, Reliable “I should have confidence, that when my

    test pass, my code is most probably is working.”
  63. Nov 23, Flexible “I should not rewrite my tests, every

    time I change something.”
  64. Nov 23, Localisable “When a test fails I should know

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

    test”
  66. Nov 23, • Easy to run • Quick • Simple

    • Reliable • Flexible • Localisable • Isolated
  67. Nov 23, test("searching", function() { var form = $('<form id="search-form"></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 = $('<input id="search-input" type="text" />').appendTo(form); assert(input.val().length == 0); input.val(''); form.submit(); assert(calls == 0); input.val('value'); var results = $('<div id="search-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); });
  68. Nov 23, • Easy to run • Quick • Simple

    • Reliable • Flexible • Localisable • Isolated
  69. Nov 23, 4 Phase testing

  70. Nov 23,

  71. Nov 23, 4 Phase testing

  72. Nov 23, Setup 4 Phase testing

  73. Nov 23, Setup Action 4 Phase testing

  74. Nov 23, Setup Action Assertion 4 Phase testing

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

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

  77. Nov 23, test("searching", function() { var form = $('<form id="search-form"></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 = $('<input id="search-input" type="text" />').appendTo(form); assert(input.val().length == 0); input.val(''); form.submit(); assert(calls == 0); input.val('value'); var results = $('<div id="search-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); });
  78. Nov 23, test("searching", function() { var form = $('<form id="search-form"></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 = $('<input id="search-input" type="text" />').appendTo(form); assert(input.val().length == 0); input.val(''); form.submit(); assert(calls == 0); input.val('value'); var results = $('<div id="search-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); });
  79. Nov 23,

  80. Nov 23,

  81. Nov 23, suite("searching", function() { var form, input, results, server;

    setup(function() { form = $('<form id="search-form"></form>').appendTo('body'); results = $('<div id="search-results" />').appendTo(form); input = $('<input id=“search-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();
  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() {
  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);
  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); }); });
  85. Nov 23,

  86. Nov 23, suite("searching", function() { var form, input, results, server;

    setup(function() { form = $('<form id="search-form"></form>').appendTo('body'); results = $('<div id="search-results" />').appendTo(form); input = $('<input id=“search-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); }); });
  87. Nov 23, test("empty search", function() { input.val(''); form.submit(); assert(server.requests.length ==

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

    });
  89. Nov 23,

  90. Nov 23,

  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); }); });
  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"]') }); });
  93. Nov 23, Mocks

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

  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.
  96. Nov 23, Dummy object Used when a parameter is needed

    for the tested method but without actually needing to use the parameter.
  97. Nov 23, Stub Test stubs are objects with pre-programmed behaviour.

  98. Nov 23, Spy Records arguments, return value, the value of

    this and exception thrown (if any) for all its calls.
  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.
  100. Nov 23, Test double Stub Spy Mock Fake Dummy

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

  102. Nov 23, Fragile tests

  103. Nov 23, Bad fixtures suite("searching", function() { var form, input,

    results; setup(function() { form = $('<form id="search-form"></form>').appendTo('body') results = $('<div id="search-results" />').appendTo(form); input = $('<input id=“search-input" />').appendTo(form); input.val('query'); app.initSearch(); }); /* teardown(function() { form.remove(); }); */ }); ✗
  104. Nov 23, Bad fixtures suite("searching", function() { var form, input,

    results; setup(function() { form = $('<form id="search-form"></form>').appendTo('body') results = $('<div id="search-results" />').appendTo(form); input = $('<input id=“search-input" />').appendTo(form); input.val('query'); app.initSearch(); }); teardown(function() { form.remove(); }); }); ✔
  105. Nov 23, Bad fixtures var input = $('<input id="search-input" type="text"

    />').appendTo(form); assert(input.val().length == 0); ✗
  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) }); }); }); ✗
  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) }); }); }); ✔
  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); }); }); ✗
  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) }); }); ✔
  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); }); }); ✔
  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); }); }); ✗
  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
  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; }); }); ✗
  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); }); }); ✗
  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; }); }); ✔
  116. Nov 23, JavaScript tips

  117. Nov 23, AJAX

  118. Nov 23, var callback; var calls = 0; $.getJSON =

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

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

    function(url, success) { assert(url === '/search?q=value'); callback = success; calls += 1 ; }
  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));
  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('<a href="/products/' + item.id + ' }); }); }); };
  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('<a href="/products/' + item.id + '"> }); }); $('#search-form').on('submit', function(e) { e.preventDefault(); search.newQuery($('#search-input').val().trim()); }); };
  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(); }); });
  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('<a href="/products/' + item.id + '"> }); }); $('#search-form').on('submit', function(e) { e.preventDefault(); search.newQuery($('#search-input').val().trim()); }); };
  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'}
  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"]') });
  128. Nov 23, DOM

  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'); }); });
  130. Nov 23, jQuery Spaghetti

  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
  132. Nov 23, Tools

  133. Nov 23,

  134. Nov 23,

  135. Nov 23,

  136. Nov 23,

  137. Nov 23,

  138. Nov 23,

  139. Nov 23,

  140. Nov 23,

  141. Nov 23,

  142. Nov 23, Books

  143. Nov 23,

  144. Nov 23,

  145. Nov 23,

  146. Nov 23, Summary

  147. Nov 23, • Test terminology • 4 Phase testing •

    Mocks • Fragile tests • Tips and Tricks • Tools and Books Summary
  148. Nov 23, https://speakerdeck.com/rstankov/testing-javascript Slides

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

  150. Nov 23, Thanks

  151. Nov 23, Questions?