why cant i test my javascript - assets.en.oreilly.comassets.en.oreilly.com/1/event/59/why can_t i...
TRANSCRIPT
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() { }});