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” };
  2. Nov 23, • 2002 - first “professional" project (Flash) •

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

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

    2003 - first JavaScript “production” code • 2008 - first PHP unit test • 2009 - first Ruby unit test My “history”
  5. 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”
  6. 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”
  7. 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”
  8. 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”
  9. 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); }); });
  10. 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); }); });
  11. 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); }); });
  12. 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); }); });
  13. 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); }); });
  14. 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); }); });
  15. 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); }); });
  16. 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); }); });
  17. 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); }); });
  18. 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); }); });
  19. 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); }); });
  20. 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); }); });
  21. 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
  22. 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); }); });
  23. 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
  24. 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); }); });
  25. 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
  26. 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); }); });
  27. 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
  28. 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); }); });
  29. 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
  30. 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>'); }); }); }); });
  31. 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>'); }); }); }); };
  32. 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); });
  33. 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');
  34. 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([
  35. 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); });
  36. Nov 23, Reliable “I should have confidence, that when my

    test pass, my code is most probably is working.”
  37. Nov 23, • Easy to run • Quick • Simple

    • Reliable • Flexible • Localisable • Isolated
  38. 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); });
  39. Nov 23, • Easy to run • Quick • Simple

    • Reliable • Flexible • Localisable • Isolated
  40. 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); });
  41. 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); });
  42. 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();
  43. 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() {
  44. 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);
  45. 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); }); });
  46. 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); }); });
  47. 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); }); });
  48. 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"]') }); });
  49. Nov 23, Fake Used as a simpler implementation, e.g. using

    an in-memory database in the tests instead of doing real database access.
  50. Nov 23, Dummy object Used when a parameter is needed

    for the tested method but without actually needing to use the parameter.
  51. Nov 23, Spy Records arguments, return value, the value of

    this and exception thrown (if any) for all its calls.
  52. 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.
  53. 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(); }); */ }); ✗
  54. 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(); }); }); ✔
  55. Nov 23, Bad fixtures var input = $('<input id="search-input" type="text"

    />').appendTo(form); assert(input.val().length == 0); ✗
  56. 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) }); }); }); ✗
  57. 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) }); }); }); ✔
  58. 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); }); }); ✗
  59. 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) }); }); ✔
  60. 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); }); }); ✔
  61. 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); }); }); ✗
  62. 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
  63. 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; }); }); ✗
  64. 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); }); }); ✗
  65. 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; }); }); ✔
  66. Nov 23, var callback; var calls = 0; $.getJSON =

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

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

    function(url, success) { assert(url === '/search?q=value'); callback = success; calls += 1 ; }
  69. 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));
  70. 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 + ' }); }); }); };
  71. 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()); }); };
  72. 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(); }); });
  73. 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()); }); };
  74. 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'}
  75. 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"]') });
  76. 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'); }); });
  77. 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
  78. Nov 23, • Test terminology • 4 Phase testing •

    Mocks • Fragile tests • Tips and Tricks • Tools and Books Summary