Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Nov 23, Reasons for automated testings

Slide 4

Slide 4 text

Nov 23, Test driven development

Slide 5

Slide 5 text

Nov 23, Test-induced design damage

Slide 6

Slide 6 text

Nov 23, Philosophy surrounding testing

Slide 7

Slide 7 text

Nov 23, Selling testing

Slide 8

Slide 8 text

Nov 23, I’m going to about

Slide 9

Slide 9 text

Nov 23,

Slide 10

Slide 10 text

Nov 23, Hard to find things

Slide 11

Slide 11 text

Nov 23, Who am I?

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Nov 23,

Slide 14

Slide 14 text

Nov 23, My “history”

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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”

Slide 20

Slide 20 text

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”

Slide 21

Slide 21 text

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”

Slide 22

Slide 22 text

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”

Slide 23

Slide 23 text

Nov 23,

Slide 24

Slide 24 text

Nov 23, What is a self-testing code?

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

Nov 23,

Slide 35

Slide 35 text

Nov 23,

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

Nov 23, Rspec Dave Astels David Chelimsky
 inspired by JBehave in 2008

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

Nov 23,

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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([

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

Nov 23,

Slide 58

Slide 58 text

Nov 23, What is a good test?

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Nov 23, 4 Phase testing

Slide 70

Slide 70 text

Nov 23,

Slide 71

Slide 71 text

Nov 23, 4 Phase testing

Slide 72

Slide 72 text

Nov 23, Setup 4 Phase testing

Slide 73

Slide 73 text

Nov 23, Setup Action 4 Phase testing

Slide 74

Slide 74 text

Nov 23, Setup Action Assertion 4 Phase testing

Slide 75

Slide 75 text

Nov 23, Setup Action Assertion Teardown 4 Phase testing

Slide 76

Slide 76 text

Nov 23, Setup Action Assertion Teardown 4 Phase testing

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

Nov 23,

Slide 80

Slide 80 text

Nov 23,

Slide 81

Slide 81 text

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();

Slide 82

Slide 82 text

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() {

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

Nov 23,

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

Nov 23,

Slide 90

Slide 90 text

Nov 23,

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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"]') }); });

Slide 93

Slide 93 text

Nov 23, Mocks

Slide 94

Slide 94 text

Nov 23, Mock Objects: friends of foes?

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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.

Slide 100

Slide 100 text

Nov 23, Test double Stub Spy Mock Fake Dummy

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

Nov 23, Fragile tests

Slide 103

Slide 103 text

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(); }); */ }); ✗

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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) }); }); }); ✗

Slide 107

Slide 107 text

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) }); }); }); ✔

Slide 108

Slide 108 text

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); }); }); ✗

Slide 109

Slide 109 text

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) }); }); ✔

Slide 110

Slide 110 text

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); }); }); ✔

Slide 111

Slide 111 text

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); }); }); ✗

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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; }); }); ✗

Slide 114

Slide 114 text

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); }); }); ✗

Slide 115

Slide 115 text

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; }); }); ✔

Slide 116

Slide 116 text

Nov 23, JavaScript tips

Slide 117

Slide 117 text

Nov 23, AJAX

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

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

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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('

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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"]') });

Slide 128

Slide 128 text

Nov 23, DOM

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

Nov 23, jQuery Spaghetti

Slide 131

Slide 131 text

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

Slide 132

Slide 132 text

Nov 23, Tools

Slide 133

Slide 133 text

Nov 23,

Slide 134

Slide 134 text

Nov 23,

Slide 135

Slide 135 text

Nov 23,

Slide 136

Slide 136 text

Nov 23,

Slide 137

Slide 137 text

Nov 23,

Slide 138

Slide 138 text

Nov 23,

Slide 139

Slide 139 text

Nov 23,

Slide 140

Slide 140 text

Nov 23,

Slide 141

Slide 141 text

Nov 23,

Slide 142

Slide 142 text

Nov 23, Books

Slide 143

Slide 143 text

Nov 23,

Slide 144

Slide 144 text

Nov 23,

Slide 145

Slide 145 text

Nov 23,

Slide 146

Slide 146 text

Nov 23, Summary

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

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

Slide 150

Slide 150 text

Nov 23, Thanks

Slide 151

Slide 151 text

Nov 23, Questions?