why cant i test my javascript - assets.en.oreilly.comassets.en.oreilly.com/1/event/59/Why Can_t I...

Preview:

Citation preview

Greg MoeckWhy Can’t I Test My Javascript?

@gregmoeck

Quick Poll

Question1

Practice TDD/BDD In Ruby/Rails

Practiced TDD Before

Ruby

Question 2

Work Regularly In JavaScript

Practice TDD/BDD In JavaScript

TDD/BDD In JavaScript(No DOM)

Why You Can’t Test JavaScript

What Is The

Problem?

I’ve Heard Many

Excuses

Tools Suck

Browsers Suck

DOM Sucks

There Is Another

Issue

Our Tests Are Talking

To Us

“All Of The Pain That We Feel When Writing Unit Tests

Points At Underlying Design Problems.

Michael Feathers, The Deep Synergy Between Good

Design and Testability

Story Time

<?php...<div id= “vault_items”> ... $query1 = "SELECT * FROM storage_access_vault_items WHERE access_id = {$_GET[pid]}"; $result1 = mysql_query($query1); $inner_vault_items = array(); while($this_item = mysql_fetch_assoc($result1))   { ?> <div class= “vault_item”> <?= $this_item[‘description’] ?> ... </div><?php } ...</div>?>

What Do You Say?

You Need Structure

JS≠

CSS

How To Test

JavaScript

Simple Answer

Same As Everything

Else

What We Want In

Our Tests

Key Question

What Do Your Tests

Do?

A. Catch Bugs

B. Ensure Value

Ensure Value

Two Types Of Value

External Value

Internal Value

External Value

End-To-End Acceptance Tests

Running Them

Does It Meet The External Value?

Writing Them

Do We Understand External Value?

Acceptance Tests

Internal Quality?X

Internal Value

Isolated Unit Tests

Writing Them

Feedback On The Quality Of Code

Running Them

Tells Us We Haven’t Broken Any Object

Unit Tests

System Works Together?

X

All Objects Work Together

Acceptance Tests

All Objects Work Individually

Unit Tests

“Unit Tests Tell You That You Built The System Right.

Acceptance / Integration Tests Tell You That You’ve

Built The Right System

Gojko Adzic, Specification by Example

Acceptance Tests

Ensures Value For The User

(External Value)

Use The System As The User

Would

Scenario('Searching For An Item', function() { Given('an auction item that is available for sale', function() { var itemTitle = 'item'; beforeEach(function() { Fictum.addResource('Item', {title: itemTitle}); }); When('I search for that auction item', function() { beforeEach(function() { Simulo.fillIn('#mainSearchWidget input[type="text"]', itemTitle); Simulo.clickOn('#mainSearchWidget .submit-search'); });

Then('I should see the auction item within the search results', function(page) { page.within('.search-results', function(page) { expect(page).toHaveContent(itemTitle); }); }); }); }); });});

Scenario('Searching For An Item', function() { Given('an auction item that is available for sale', function() { var itemTitle = 'item'; beforeEach(function() { Fictum.addResource('Item', {title: itemTitle}); }); When('I search for that auction item', function() { beforeEach(function() { Simulo.fillIn('#mainSearchWidget input[type="text"]', itemTitle); Simulo.clickOn('#mainSearchWidget .submit-search'); });

Then('I should see the auction item within the search results', function(page) { page.within('.search-results', function(page) { expect(page).toHaveContent(itemTitle); }); }); }); }); });});

Scenario('Searching For An Item', function() { Given('an auction item that is available for sale', function() { var itemTitle = 'item'; beforeEach(function() { Fictum.addResource('Item', {title: itemTitle}); }); When('I search for that auction item', function() { beforeEach(function() { Simulo.fillIn('#mainSearchWidget input[type="text"]', itemTitle); Simulo.clickOn('#mainSearchWidget .submit-search'); });

Then('I should see the auction item within the search results', function(page) { page.within('.search-results', function(page) { expect(page).toHaveContent(itemTitle); }); }); }); }); });});

Scenario('Searching For An Item', function() { Given('an auction item that is available for sale', function() { var itemTitle = 'item'; beforeEach(function() { Fictum.addResource('Item', {title: itemTitle}); }); When('I search for that auction item', function() { beforeEach(function() { Simulo.fillIn('#mainSearchWidget input[type="text"]', itemTitle); Simulo.clickOn('#mainSearchWidget .submit-search'); });

Then('I should see the auction item within the search results', function(page) { page.within('.search-results', function(page) { expect(page).toHaveContent(itemTitle); }); }); }); }); });});

Scenario('Searching For An Item', function() { Given('an auction item that is available for sale', function() { var itemTitle = 'item'; beforeEach(function() { Fictum.addResource('Item', {title: itemTitle}); }); When('I search for that auction item', function() { beforeEach(function() { Simulo.fillIn('#mainSearchWidget input[type="text"]', itemTitle); Simulo.clickOn('#mainSearchWidget .submit-search'); });

Then('I should see the auction item within the search results', function(page) { page.within('.search-results', function(page) { expect(page).toHaveContent(itemTitle); }); }); }); }); });});

Scenario('Searching For An Item', function() { Given('an auction item that is available for sale', function() { var itemTitle = 'item'; beforeEach(function() { Fictum.addResource('Item', {title: itemTitle}); }); When('I search for that auction item', function() { beforeEach(function() { Simulo.fillIn('#mainSearchWidget input[type="text"]', itemTitle); Simulo.clickOn('#mainSearchWidget .submit-search'); });

Then('I should see the auction item within the search results', function(page) { page.within('.search-results', function(page) { expect(page).toHaveContent(itemTitle); }); }); }); }); });});

Scenario('Searching For An Item', function() { Given('an auction item that is available for sale', function() { var itemTitle = 'item'; beforeEach(function() { Fictum.addResource('Item', {title: itemTitle}); }); When('I search for that auction item', function() { beforeEach(function() { Simulo.fillIn('#mainSearchWidget input[type="text"]', itemTitle); Simulo.clickOn('#mainSearchWidget .submit-search'); });

Then('I should see the auction item within the search results', function(page) { page.within('.search-results', function(page) { expect(page).toHaveContent(itemTitle); }); }); }); }); });});

Scenario('Searching For An Item', function() { Given('an auction item that is available for sale', function() { var itemTitle = 'item'; beforeEach(function() { Fictum.addResource('Item', {title: itemTitle}); }); When('I search for that auction item', function() { beforeEach(function() { Simulo.fillIn('#mainSearchWidget input[type="text"]', itemTitle); Simulo.clickOn('#mainSearchWidget .submit-search'); });

Then('I should see the auction item within the search results', function(page) { page.within('.search-results', function(page) { expect(page).toHaveContent(itemTitle); }); }); }); }); });});

Poke At The System As The User

Would

Verify The System As The User

Would

Unit Tests

Ensures Value Of The Architecture

(Internal Value)

Use Object Like A

Collaborator Would

Isolate The

Object

Button View

Button View

Button

Template

Some

Controller

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is over the button’, function() { beforeEach(function() { button.set(‘isActive’, true); }); it(‘fires the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is over the button’, function() { beforeEach(function() { button.set(‘isActive’, true); }); it(‘fires the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is over the button’, function() { beforeEach(function() { button.set(‘isActive’, true); }); it(‘fires the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is over the button’, function() { beforeEach(function() { button.set(‘isActive’, true); }); it(‘fires the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is over the button’, function() { beforeEach(function() { button.set(‘isActive’, true); }); it(‘fires the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is over the button’, function() { beforeEach(function() { button.set(‘isActive’, true); }); it(‘fires the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is over the button’, function() { beforeEach(function() { button.set(‘isActive’, true); }); it(‘fires the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is not over the button’, function() { beforeEach(function() { button.set(‘isActive’, false); }); it(‘does not fire the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).not.toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is not over the button’, function() { beforeEach(function() { button.set(‘isActive’, false); }); it(‘does not fire the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).not.toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is not over the button’, function() { beforeEach(function() { button.set(‘isActive’, false); }); it(‘does not fire the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).not.toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { describe(‘when the mouse is not over the button’, function() { beforeEach(function() { button.set(‘isActive’, false); }); it(‘does not fire the button action’, function() { var actionSpy = spyOn(controller, ‘someAction’); button.mouseUp();

expect(actionSpy).not.toHaveBeenCalled(); }); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); ... describe(‘#mouseDown’, function() { it(‘it sets the button to be active’, function() { button.mouseDown();

expect(button.get(‘isActive’)).toBe(true); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); ... describe(‘#mouseDown’, function() { it(‘it sets the button to be active’, function() { button.mouseDown();

expect(button.get(‘isActive’)).toBe(true); }); });});

describe('ButtonView', function() { var button, controller; beforeEach(function() { controller = {action: function() {}}; button = ButtonView.create({controller: controller}); }); ... describe(‘#mouseDown’, function() { it(‘it sets the button to be active’, function() { button.mouseDown();

expect(button.get(‘isActive’)).toBe(true); }); });});

Ensures A Well

Defined API

Limits Dependencies

Full Example

Some List

Pending Items

Pending Todo

P1 1

Some List

Pending Items

Pending Todo

P1 1

Some List

Pending Items

Pending Todo

P1 1

Some List

Pending Items

Pending Todo

P1 0

Some List

Pending Items

Todo #1

P1 3

Todo #2

Todo #3

Complete All

Some List

Pending Items

Todo #1

P1 3

Todo #2

Todo #3

Complete All

Some List

Pending Items

Todo #1

P1 3

Todo #2

Todo #3

Complete All

Some List

Pending Items

Todo #1

P1 3

Todo #2

Todo #3

Complete All

Some List

Pending Items

Todo #1

P1 0

Todo #2

Todo #3

Complete All

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Scenario('Marking all items in a list complete', function() { Given(‘a list with multiple incomplete items’, function() { beforeEach(function() { Fictum.addResource(‘item’, {isComplete: false}); Fictum.addResource(‘item’, {isComplete: false}); }); When(‘I press the button to mark all items complete’, function() { beforeEach(function() { Simulo.clickOn(‘.select-all-button’); }); Then(‘All of the items should be marked complete’, function() { expect($(‘input:checked’).length).toBe(2); }); Then(‘I should see that no items are still pending’, function() { expect($(‘.remaining-items’).val()).toContain(0); }); }); });});

Run It

Error: Button Not

Found

App.SelectAllCompleteButtonView = SC.Button.extend({ classNames: [‘select-button-view’]});

Run It

Error: Length Of Complete

describe('App.SelectAllCompleteButtonView', function() { var button, controller; beforeEach(function() { controller = {markAllComplete: function() {}}; button = App.SelectAllCompleteButtonView.create({controller: controller}); }); describe(‘#mouseUp’, function() { it(‘fires the mark all complete action’, function() { var actionSpy = spyOn(controller, ‘markAllComplete’); button.mouseUp();

expect(actionSpy).toHaveBeenCalled(); }); }); });});

Run It

Error: Action Not

Called

App.SelectAllCompleteButtonView = SC.Button.extend({ classNames: [‘select-button-view’],

mouseUp: function(evt) { this.get(‘controller’).markAllComplete();}

});

Run It

Unit Tests Pass

Error:ControllerUndefined

App.SelectAllCompleteButtonView = SC.Button.extend({ classNames: [‘select-button-view’],

init: function() { if(!this.get(‘controller’)) this.set(‘controller’, App.listController); },

mouseUp: function(evt) { this.get(‘controller’).markAllComplete();}

});

Run It

Unit Tests Pass

Error:Unknown Method

App.listController = SC.ArrayController.create({ ... markAllComplete: function() { }});

Recommended