Stop Making Excuses and
Start Testing Your JavaScript!
Stop Making!
Excuses and!
Start Testing !
Your !
JavaScript!
• Senior UI Engineer at Netflix
• ryan.anklam@gmail.com
• @bittersweeryan
About Me
Today’s Agenda
• Adding Testing to Your Project
• Solving Common Testing Issues
• Writing Testable JavaScript
• Automating Testing
Adding Testing To Your
Project
Step 1: Pick Your Environment
TapeJest
Browser
Jest
Step 2: Pick Your Dialect
expect
describe( 'adder', function(){
it( 'should return zero when no args...',
function(){
expect( adder() ).to.equal( 0 );
} );
} );
+
should
describe( 'adder', function(){
it( 'should return zero when no args...',
function(){
adder().should.equal( 0 );
} );
} );
+
assert
+
//qunit
test( 'adder', function(){
ok( adder() === 0, 'should return 0');
} );
!
//chai
describe( 'adder', function(){
assert.equal( adder() , 0, 'should return 0' );
});
Step 3: Setup
$> npm install -g mocha
$> npm install mocha —save-dev
$> npm install chai --save-dev
module.exports = var adder = function(){
var args = Array.prototype.slice.call( arguments );
return args.reduce( function( previous, curr ){
if( !isNaN( Number( curr ) ) ){
return previous + curr;
}
else{
return curr;
}
}, 0 );
};
adder.js
var expect = require( 'chai' ).expect;
var adder = require( '../../src/adder' );
!
describe( 'adder', function(){
it( 'should add an array of numbers',
function(){
expect( adder( 1,2,3) ).to.equal( 6 );
});
});
adderSpec.js
...
describe( 'adder', function(){
it( 'should return zero if no args are passed
in', function(){
expect( adder( ) ).to.equal(0);
});
})
...
adderSpec.js
...
it( 'should skip non numbers', function(){
expect( adder( 1,2,'a' ) ).to.equal( 3 );
});
...
adderSpec.js
$> mocha tests/spec
Name of your spec directory
Browser
Browser
<html>
<head>
<title>Jasmine Spec Runner v2.0.0</title>
<link rel="stylesheet" type="text/css" href="lib/jasmine-2.0.0/
jasmine.css">
!
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></
script>
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine-
html.js"></script>
<script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></
script>
!
<!-- include source files here... -->
<script type="text/javascript" src="../src/adder.js"></script>
<!-- include spec files here... -->
<script type="text/javascript" src="spec/adderSpec-jasmine.js"></
script>
</head>
<body>
</body>
</html>
SpecRunner.html
Source Files
Spec Files
Browser
Solving Common
Testing Issues
I Have Methods that Call Other
Methods.
Spies/Stubs/Mocks
Spies
User.prototype.addUser = function(){
if(validateUser()){
saveUser();
}
else{
addError( 'user not valid' );
}
};
Spies
it('should save a valid user', function(){
var user = new User( 'Ryan','Anklam');
spyOn(User, ‘validate’).and.returnValue( true );
spyOn(User, 'save');
user.add();
expect(user.validate).toHaveBeenCalled();
expect(user.save).toHaveBeenCalled();
});
Spies
it('should error for an invalid user', function(){
var user = new User( 'Ryan','Anklam');
spyOn(User, ‘validate’).andReturn( false );
spyOn(User, 'addError');
user.add();
expect(user.validate).toHaveBeenCalled();
expect(user.addError).toHaveBeenCalled();
});
Spies That Return Data
User.prototype.listUsers = function(){
var users = this.getUsers();
users.forEach( function( user ){
this.addUserToList( user );
});
};
Spies That Return Data
it('should get a list of users and add them',
function(){
spyOn(User, 'getUsers').and.returnValue(
[
{
firstName : 'Ryan',
lastName : 'Anklam'
}
]
);
spyOn(User, 'addUserToList');
user.listUsers();
expect(user.getUsers).toHaveBeenCalled();
expect(user.addUserToList).toHaveBeenCalled();
});
Promises
Returns A Promise
getStringValue: function( req, bundle, key, context ){
var i18n = require("./index"),
bundlePromise = this.getBundle( req, bundle ),
deferred = q.defer();
!
bundlePromise.then(
function( bundle ){
deferred.resolve(
bundle.get( i18n.get(key,context) ) );
},
function( err ){
deferred.reject( err );
}
);
!
return deferred.promise;
}
chai-as-promised FTW!
describe( 'getStringValue function', function(){
!
it('should return a string when it finds a key',
function ( ) {
return expect( utils.getStringValue( 'foo.bar.baz' ) )
.to.eventually.equal( 'Foo bar baz'] );
});
!
it('should return the key when no key is found',
function () {
return expect( utils.getStringValue( 'does.not.exist' ) )
.to.eventually.equal( 'does.not.exist' );
});
} );
I Need To Test the DOM
Do you REALLY need to?
Spy On The DOM
function addItems( $el, items ){
items.forEach( function( item ){
$el.append( item );
} );
}
Spy On The DOM
it( 'should add items', function(){
var $el = $( '<ul/>' ),
items = [
{ name : 'test' }
];
spyOn( jQuery.fn, 'append' );
!
addItems( $el, items );
expect( jQuery.fn.append )
.toHaveBeenCalled();
} );
Jasmine jQuery
https://github.com/velesin/jasmine-jquery
Create Fixtures as .html files
Load fixtures into a test
Make assertions
Jasmine jQuery
beforeEach( function(){
//reset the fixture before each test
loadFixtures('myfixture.html');
});
!
it( 'should convert a select to a ul', function(){
$( '.select' ).dropdown();
expect( '.select-list' ).toExist();
});
qUnit
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>QUnit Example</title>
<link rel="stylesheet" href="qunit.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="qunit.js"></script>
<script src="tests.js"></script>
</body>
</html>
Gets reset after
every test
I Have to Test Async Code
Async Test
function readConfig( callback ){
!
fs.readFile( config.fileLocation, callback );
}
Async Test
it( 'should read the config file' ,
function( done ){
var callback = function( readystate ){
expect( readystate )
.to.equal( 'config ready' );
done();
}
readConfig( callback );
} );
Async Test
asyncTest( 'should read the config file' ,
function( ){
var callback = function( readystate ){
ok( readystate === 'config ready' );
start();
}
readConfig( callback );
} );
Writing Testable
JavaScript
Not Testable JavaScript
$(function(){
var $list = $( '.todo-list' ),
$input = $( '.new-todo' ),
todos = $.ajax(...);
todos.forEach( function( item, index){
$list.append( '<li class="todo-item">' + item.name +
'</li>');
} );
$input.on( 'keyup', function( e ){
if( e.which === 13 ){
$list.append( '<li class="todo-item">' +
$(this).val() + '</li>');
$(this).val( '' );
}
} );
Repeated Code
Locked in callback
Magic Data
Can’t configure
One and doneAnonymous Function
Anonymous Function
Refactoring
$(function(){
var $list = $( '.todo-list' ),
$input = $( '.new-todo' ),
todos = $.ajax(...);
todos.forEach( function( item, index){
$list.append( '<li class="todo-item">' + item.name +
'</li>');
} );
$input.on( 'keyup', function( e ){
if( e.which === 13 ){
$list.append( '<li class="todo-item">' +
$(this).val() + '</li>');
$(this).val( '' );
}
} );
Can’t configure
Make It A Constructor
var Todos = function( $list, $input ){
this.$list = $list;
this.$input = $input;
};
A Little Setup & First Test
describe( 'todos', function(){
var todos;
beforeEach( function(){
todos = new Todos( $('<ul/>'),
$('<input type="text" value="foobar" />')
);
});
} );
it('should create an instance', function(){
expect( typeof todos).toEqual( 'object' );
});
Not Testable JavaScript
$(function(){
var $list = $( '.todo-list' ),
$input = $( '.new-todo' ),
todos = $.ajax(...);
todos.forEach( function( item, index){
$list.append( '<li class="todo-item">' + item.name +
'</li>');
} );
$input.on( 'keyup', function( e ){
if( e.which === 13 ){
$list.append( '<li class="todo-item">' +
$(this).val() + '</li>');
$(this).val( '' );
}
} );
Magic Data
getData
Todos.prototype.getData = function( success, error ){
$.ajax({
...
success : success,
error : error
...
});
}
Test getData
it(‘should have a getData fn that returns an array',
function( done ){
var callback = function( items ){
expect(items instanceof Array ).toBeTruthy();
done();
};
todos.getData( callback );
});
**Note: more tests would be to properly test this!**
Refactoring
$(function(){
var $list = $( '.todo-list' ),
$input = $( '.new-todo' ),
todos = $.ajax(...);
todos.forEach( function( item, index){
$list.append( '<li class="todo-item">' + item.name +
'</li>');
} );
$input.on( 'keyup', function( e ){
if( e.which === 13 ){
$list.append( '<li class="todo-item">' +
$(this).val() + '</li>');
$(this).val( '' );
}
} );
Anonymous Function
Refactoring
$(function(){
var $list = $( '.todo-list' ),
$input = $( '.new-todo' ),
todos = $.ajax(...);
todos.forEach( function( item, index){
$list.append( '<li class="todo-item">' + item.name +
'</li>');
} );
$input.on( 'keyup', function( e ){
if( e.which === 13 ){
$list.append( '<li class="todo-item">' +
$(this).val() + '</li>');
$(this).val( '' );
}
} );
One and done
Refactoring
$(function(){
var $list = $( '.todo-list' ),
$input = $( '.new-todo' ),
todos = $.ajax(...);
todos.forEach( function( item, index){
$list.append( '<li class="todo-item">' + item.name +
'</li>');
} );
$input.on( 'keyup', function( e ){
if( e.which === 13 ){
$list.append( '<li class="todo-item">' +
$(this).val() + '</li>');
$(this).val( '' );
}
} );
Repeated Code
addItem
Todos.prototype.addItem = function( name ){
this.$list.append(
’<li class="todo-item">' + name + ‘</li>'
);
};
Test addItem
it('should have a addItem function', function(){
expect( typeof todos.addItem ).toEqual( 'function' );
});
!
it('should try to add an item to the DOM', function(){
spyOn( jQuery.fn, 'append' );
todos.addItem( 'Learn JS Testing' );
expect( jQuery.fn.append )
.toHaveBeenCalledWith(
'<li class="todo-item">Learn JS Testing</li>'
);
});
addItems
Todos.prototype.addItems = function( items ){
items.forEach( function( item, index){
this.addItem( item.name );
}, this );
};
Test addItems
it('should have a addItems function', function(){
spyOn( todos, 'addItem' );
todos.addItems( [{name:'foo'},{name:'bar'},{name:'baz'}] );
expect( todos.addItem ).toHaveBeenCalledWith( 'foo' );
expect( todos.addItem ).toHaveBeenCalledWith( 'bar' );
expect( todos.addItem ).toHaveBeenCalledWith( 'baz' );
});
Not Testable JavaScript
$(function(){
var $list = $( '.todo-list' ),
$input = $( '.new-todo' ),
todos = $.ajax(...);
todos.forEach( function( item, index){
$list.append( '<li class="todo-item">' + item.name +
'</li>');
} );
$input.on( 'keyup', function( e ){
if( e.which === 13 ){
$list.append( '<li class="todo-item">' +
$(this).val() + '</li>');
$(this).val( '' );
}
} );
Anonymous Function
handleKeyPress
Todos.prototype.handleKeypress = function( e ){
if( e.which === 13 ){
this.addItem( this.$input.val() );
this.$input.val( '' );
}
};
Test handleKeypress
it('should have a handleKeypress function', function(){
expect( typeof todos.handleKeypress )
.toEqual( 'function' );
});
!
it('should handle a keypress', function(){
spyOn( todos, 'addItem' );
todos.handleKeypress( { which: 13 } );
expect( todos.addItem )
.toHaveBeenCalledWith( 'foobar' );
});
Refactoring
$(function(){
var $list = $( '.todo-list' ),
$input = $( '.new-todo' ),
todos = $.ajax(...);
todos.forEach( function( item, index){
$list.append( '<li class="todo-item">' + item.name +
'</li>');
} );
$input.on( 'keyup', function( e ){
if( e.which === 13 ){
$list.append( '<li class="todo-item">' +
$(this).val() + '</li>');
$(this).val( '' );
}
} );
Locked in callback
Init function
Todos.prototype.init = function(){
this.$input.on( 'keyup',
$.proxy( this.handleKeypress, this ) );
this.getData( this.addItems );
};
Test Init
it('should have an init method', function(){
expect( typeof todos.init ).toEqual( 'function' );
});
!
it('should setup a handler and additems', function(){
spyOn( jQuery.fn, 'on' );
spyOn( todos, 'getData' );
todos.init();
expect( jQuery.fn.on ).toHaveBeenCalled();
expect( todos.getData )
.toHaveBeenCalledWith( todos.addItems );
} );
A Few More Tips
• Write maintainable tests
• Avoid Singletons
• Use named functions instead of anonymous

callbacks
• Keep your functions small
Automating Testing
The key to successful testing is
making writing and running tests
easy.
The key to successful testing is
making writing and running tests
easy.
The key to successful testing is
making writing and running tests
easy.
The key to successful testing is
making writing and running tests
easy.
Using NPM
{
...
"scripts": {
"test": "mocha tests/spec"
},
...
}
Package.json
Running Build Tests
$> npm test
Using Your Build Tool
npm install -g grunt-cli
npm install grunt-contrib-jasmine
npm install grunt
gruntfile.js
module.exports = function(grunt) {
grunt.initConfig({
jasmine : {
tests: {
src: ['jquery.js','todos-better.js'],
options: { specs: 'tests/spec/todoSpec.js' }
}
}
watch: {
tests: {
files: ['**/*.js'],
tasks: ['jasmine'],
}
}
});
grunt.loadNpmTasks('grunt-contrib-jasmine', ‘watch' );
};
Running Build Tests
$> grunt jasmine
Testem
npm install -g testem
testem.json
{
"src_files" : [
"jquery.js",
"todos-better.js",
"tests/spec/todoSpec.js"
],
"launch_in_dev" : [ "PhantomJS", “chrome”, “firefox” ],
"framework" : "jasmine2"
}
Testem
$> testem
Questions?
Thank You
ryan.anklam@gmail.com
@bittersweetryan

Stop Making Excuses and Start Testing Your JavaScript

  • 1.
    Stop Making Excusesand Start Testing Your JavaScript! Stop Making! Excuses and! Start Testing ! Your ! JavaScript!
  • 2.
    • Senior UIEngineer at Netflix • ryan.anklam@gmail.com • @bittersweeryan About Me
  • 3.
    Today’s Agenda • AddingTesting to Your Project • Solving Common Testing Issues • Writing Testable JavaScript • Automating Testing
  • 4.
    Adding Testing ToYour Project
  • 5.
    Step 1: PickYour Environment
  • 6.
  • 7.
  • 8.
    Step 2: PickYour Dialect
  • 9.
    expect describe( 'adder', function(){ it('should return zero when no args...', function(){ expect( adder() ).to.equal( 0 ); } ); } ); +
  • 10.
    should describe( 'adder', function(){ it('should return zero when no args...', function(){ adder().should.equal( 0 ); } ); } ); +
  • 11.
    assert + //qunit test( 'adder', function(){ ok(adder() === 0, 'should return 0'); } ); ! //chai describe( 'adder', function(){ assert.equal( adder() , 0, 'should return 0' ); });
  • 12.
  • 14.
    $> npm install-g mocha $> npm install mocha —save-dev $> npm install chai --save-dev
  • 15.
    module.exports = varadder = function(){ var args = Array.prototype.slice.call( arguments ); return args.reduce( function( previous, curr ){ if( !isNaN( Number( curr ) ) ){ return previous + curr; } else{ return curr; } }, 0 ); }; adder.js
  • 16.
    var expect =require( 'chai' ).expect; var adder = require( '../../src/adder' ); ! describe( 'adder', function(){ it( 'should add an array of numbers', function(){ expect( adder( 1,2,3) ).to.equal( 6 ); }); }); adderSpec.js
  • 17.
    ... describe( 'adder', function(){ it('should return zero if no args are passed in', function(){ expect( adder( ) ).to.equal(0); }); }) ... adderSpec.js
  • 18.
    ... it( 'should skipnon numbers', function(){ expect( adder( 1,2,'a' ) ).to.equal( 3 ); }); ... adderSpec.js
  • 19.
    $> mocha tests/spec Nameof your spec directory
  • 21.
  • 22.
    Browser <html> <head> <title>Jasmine Spec Runnerv2.0.0</title> <link rel="stylesheet" type="text/css" href="lib/jasmine-2.0.0/ jasmine.css"> ! <script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></ script> <script type="text/javascript" src="lib/jasmine-2.0.0/jasmine- html.js"></script> <script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></ script> ! <!-- include source files here... --> <script type="text/javascript" src="../src/adder.js"></script> <!-- include spec files here... --> <script type="text/javascript" src="spec/adderSpec-jasmine.js"></ script> </head> <body> </body> </html> SpecRunner.html Source Files Spec Files
  • 23.
  • 24.
  • 25.
    I Have Methodsthat Call Other Methods.
  • 26.
  • 28.
  • 29.
    Spies it('should save avalid user', function(){ var user = new User( 'Ryan','Anklam'); spyOn(User, ‘validate’).and.returnValue( true ); spyOn(User, 'save'); user.add(); expect(user.validate).toHaveBeenCalled(); expect(user.save).toHaveBeenCalled(); });
  • 30.
    Spies it('should error foran invalid user', function(){ var user = new User( 'Ryan','Anklam'); spyOn(User, ‘validate’).andReturn( false ); spyOn(User, 'addError'); user.add(); expect(user.validate).toHaveBeenCalled(); expect(user.addError).toHaveBeenCalled(); });
  • 31.
    Spies That ReturnData User.prototype.listUsers = function(){ var users = this.getUsers(); users.forEach( function( user ){ this.addUserToList( user ); }); };
  • 32.
    Spies That ReturnData it('should get a list of users and add them', function(){ spyOn(User, 'getUsers').and.returnValue( [ { firstName : 'Ryan', lastName : 'Anklam' } ] ); spyOn(User, 'addUserToList'); user.listUsers(); expect(user.getUsers).toHaveBeenCalled(); expect(user.addUserToList).toHaveBeenCalled(); });
  • 33.
  • 34.
    Returns A Promise getStringValue:function( req, bundle, key, context ){ var i18n = require("./index"), bundlePromise = this.getBundle( req, bundle ), deferred = q.defer(); ! bundlePromise.then( function( bundle ){ deferred.resolve( bundle.get( i18n.get(key,context) ) ); }, function( err ){ deferred.reject( err ); } ); ! return deferred.promise; }
  • 35.
    chai-as-promised FTW! describe( 'getStringValuefunction', function(){ ! it('should return a string when it finds a key', function ( ) { return expect( utils.getStringValue( 'foo.bar.baz' ) ) .to.eventually.equal( 'Foo bar baz'] ); }); ! it('should return the key when no key is found', function () { return expect( utils.getStringValue( 'does.not.exist' ) ) .to.eventually.equal( 'does.not.exist' ); }); } );
  • 36.
    I Need ToTest the DOM
  • 37.
    Do you REALLYneed to?
  • 39.
    Spy On TheDOM function addItems( $el, items ){ items.forEach( function( item ){ $el.append( item ); } ); }
  • 40.
    Spy On TheDOM it( 'should add items', function(){ var $el = $( '<ul/>' ), items = [ { name : 'test' } ]; spyOn( jQuery.fn, 'append' ); ! addItems( $el, items ); expect( jQuery.fn.append ) .toHaveBeenCalled(); } );
  • 41.
    Jasmine jQuery https://github.com/velesin/jasmine-jquery Create Fixturesas .html files Load fixtures into a test Make assertions
  • 42.
    Jasmine jQuery beforeEach( function(){ //resetthe fixture before each test loadFixtures('myfixture.html'); }); ! it( 'should convert a select to a ul', function(){ $( '.select' ).dropdown(); expect( '.select-list' ).toExist(); });
  • 43.
    qUnit <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>QUnitExample</title> <link rel="stylesheet" href="qunit.css"> </head> <body> <div id="qunit"></div> <div id="qunit-fixture"></div> <script src="qunit.js"></script> <script src="tests.js"></script> </body> </html> Gets reset after every test
  • 44.
    I Have toTest Async Code
  • 45.
    Async Test function readConfig(callback ){ ! fs.readFile( config.fileLocation, callback ); }
  • 46.
    Async Test it( 'shouldread the config file' , function( done ){ var callback = function( readystate ){ expect( readystate ) .to.equal( 'config ready' ); done(); } readConfig( callback ); } );
  • 47.
    Async Test asyncTest( 'shouldread the config file' , function( ){ var callback = function( readystate ){ ok( readystate === 'config ready' ); start(); } readConfig( callback ); } );
  • 48.
  • 49.
    Not Testable JavaScript $(function(){ var$list = $( '.todo-list' ), $input = $( '.new-todo' ), todos = $.ajax(...); todos.forEach( function( item, index){ $list.append( '<li class="todo-item">' + item.name + '</li>'); } ); $input.on( 'keyup', function( e ){ if( e.which === 13 ){ $list.append( '<li class="todo-item">' + $(this).val() + '</li>'); $(this).val( '' ); } } ); Repeated Code Locked in callback Magic Data Can’t configure One and doneAnonymous Function Anonymous Function
  • 50.
    Refactoring $(function(){ var $list =$( '.todo-list' ), $input = $( '.new-todo' ), todos = $.ajax(...); todos.forEach( function( item, index){ $list.append( '<li class="todo-item">' + item.name + '</li>'); } ); $input.on( 'keyup', function( e ){ if( e.which === 13 ){ $list.append( '<li class="todo-item">' + $(this).val() + '</li>'); $(this).val( '' ); } } ); Can’t configure
  • 51.
    Make It AConstructor var Todos = function( $list, $input ){ this.$list = $list; this.$input = $input; };
  • 52.
    A Little Setup& First Test describe( 'todos', function(){ var todos; beforeEach( function(){ todos = new Todos( $('<ul/>'), $('<input type="text" value="foobar" />') ); }); } ); it('should create an instance', function(){ expect( typeof todos).toEqual( 'object' ); });
  • 53.
    Not Testable JavaScript $(function(){ var$list = $( '.todo-list' ), $input = $( '.new-todo' ), todos = $.ajax(...); todos.forEach( function( item, index){ $list.append( '<li class="todo-item">' + item.name + '</li>'); } ); $input.on( 'keyup', function( e ){ if( e.which === 13 ){ $list.append( '<li class="todo-item">' + $(this).val() + '</li>'); $(this).val( '' ); } } ); Magic Data
  • 54.
    getData Todos.prototype.getData = function(success, error ){ $.ajax({ ... success : success, error : error ... }); }
  • 55.
    Test getData it(‘should havea getData fn that returns an array', function( done ){ var callback = function( items ){ expect(items instanceof Array ).toBeTruthy(); done(); }; todos.getData( callback ); }); **Note: more tests would be to properly test this!**
  • 56.
    Refactoring $(function(){ var $list =$( '.todo-list' ), $input = $( '.new-todo' ), todos = $.ajax(...); todos.forEach( function( item, index){ $list.append( '<li class="todo-item">' + item.name + '</li>'); } ); $input.on( 'keyup', function( e ){ if( e.which === 13 ){ $list.append( '<li class="todo-item">' + $(this).val() + '</li>'); $(this).val( '' ); } } ); Anonymous Function
  • 57.
    Refactoring $(function(){ var $list =$( '.todo-list' ), $input = $( '.new-todo' ), todos = $.ajax(...); todos.forEach( function( item, index){ $list.append( '<li class="todo-item">' + item.name + '</li>'); } ); $input.on( 'keyup', function( e ){ if( e.which === 13 ){ $list.append( '<li class="todo-item">' + $(this).val() + '</li>'); $(this).val( '' ); } } ); One and done
  • 58.
    Refactoring $(function(){ var $list =$( '.todo-list' ), $input = $( '.new-todo' ), todos = $.ajax(...); todos.forEach( function( item, index){ $list.append( '<li class="todo-item">' + item.name + '</li>'); } ); $input.on( 'keyup', function( e ){ if( e.which === 13 ){ $list.append( '<li class="todo-item">' + $(this).val() + '</li>'); $(this).val( '' ); } } ); Repeated Code
  • 59.
    addItem Todos.prototype.addItem = function(name ){ this.$list.append( ’<li class="todo-item">' + name + ‘</li>' ); };
  • 60.
    Test addItem it('should havea addItem function', function(){ expect( typeof todos.addItem ).toEqual( 'function' ); }); ! it('should try to add an item to the DOM', function(){ spyOn( jQuery.fn, 'append' ); todos.addItem( 'Learn JS Testing' ); expect( jQuery.fn.append ) .toHaveBeenCalledWith( '<li class="todo-item">Learn JS Testing</li>' ); });
  • 61.
    addItems Todos.prototype.addItems = function(items ){ items.forEach( function( item, index){ this.addItem( item.name ); }, this ); };
  • 62.
    Test addItems it('should havea addItems function', function(){ spyOn( todos, 'addItem' ); todos.addItems( [{name:'foo'},{name:'bar'},{name:'baz'}] ); expect( todos.addItem ).toHaveBeenCalledWith( 'foo' ); expect( todos.addItem ).toHaveBeenCalledWith( 'bar' ); expect( todos.addItem ).toHaveBeenCalledWith( 'baz' ); });
  • 63.
    Not Testable JavaScript $(function(){ var$list = $( '.todo-list' ), $input = $( '.new-todo' ), todos = $.ajax(...); todos.forEach( function( item, index){ $list.append( '<li class="todo-item">' + item.name + '</li>'); } ); $input.on( 'keyup', function( e ){ if( e.which === 13 ){ $list.append( '<li class="todo-item">' + $(this).val() + '</li>'); $(this).val( '' ); } } ); Anonymous Function
  • 64.
    handleKeyPress Todos.prototype.handleKeypress = function(e ){ if( e.which === 13 ){ this.addItem( this.$input.val() ); this.$input.val( '' ); } };
  • 65.
    Test handleKeypress it('should havea handleKeypress function', function(){ expect( typeof todos.handleKeypress ) .toEqual( 'function' ); }); ! it('should handle a keypress', function(){ spyOn( todos, 'addItem' ); todos.handleKeypress( { which: 13 } ); expect( todos.addItem ) .toHaveBeenCalledWith( 'foobar' ); });
  • 66.
    Refactoring $(function(){ var $list =$( '.todo-list' ), $input = $( '.new-todo' ), todos = $.ajax(...); todos.forEach( function( item, index){ $list.append( '<li class="todo-item">' + item.name + '</li>'); } ); $input.on( 'keyup', function( e ){ if( e.which === 13 ){ $list.append( '<li class="todo-item">' + $(this).val() + '</li>'); $(this).val( '' ); } } ); Locked in callback
  • 67.
    Init function Todos.prototype.init =function(){ this.$input.on( 'keyup', $.proxy( this.handleKeypress, this ) ); this.getData( this.addItems ); };
  • 68.
    Test Init it('should havean init method', function(){ expect( typeof todos.init ).toEqual( 'function' ); }); ! it('should setup a handler and additems', function(){ spyOn( jQuery.fn, 'on' ); spyOn( todos, 'getData' ); todos.init(); expect( jQuery.fn.on ).toHaveBeenCalled(); expect( todos.getData ) .toHaveBeenCalledWith( todos.addItems ); } );
  • 69.
    A Few MoreTips • Write maintainable tests • Avoid Singletons • Use named functions instead of anonymous
 callbacks • Keep your functions small
  • 70.
  • 71.
    The key tosuccessful testing is making writing and running tests easy.
  • 72.
    The key tosuccessful testing is making writing and running tests easy.
  • 73.
    The key tosuccessful testing is making writing and running tests easy.
  • 74.
    The key tosuccessful testing is making writing and running tests easy.
  • 75.
    Using NPM { ... "scripts": { "test":"mocha tests/spec" }, ... } Package.json
  • 76.
  • 77.
    Using Your BuildTool npm install -g grunt-cli npm install grunt-contrib-jasmine npm install grunt
  • 78.
    gruntfile.js module.exports = function(grunt){ grunt.initConfig({ jasmine : { tests: { src: ['jquery.js','todos-better.js'], options: { specs: 'tests/spec/todoSpec.js' } } } watch: { tests: { files: ['**/*.js'], tasks: ['jasmine'], } } }); grunt.loadNpmTasks('grunt-contrib-jasmine', ‘watch' ); };
  • 79.
  • 80.
  • 81.
    testem.json { "src_files" : [ "jquery.js", "todos-better.js", "tests/spec/todoSpec.js" ], "launch_in_dev": [ "PhantomJS", “chrome”, “firefox” ], "framework" : "jasmine2" }
  • 82.
  • 83.
  • 84.