Stop Making Excuses and Start Testing Your JavaScript

84
Stop Making Excuses and Start Testing Your JavaScript! Stop Making Excuses and Start Testing Your JavaScript!

description

Talk from HTML5DevConf about JavaScript unit testing.

Transcript of Stop Making Excuses and Start Testing Your JavaScript

Page 1: Stop Making Excuses and Start Testing Your JavaScript

Stop Making Excuses and Start Testing Your JavaScript!

Stop Making!Excuses and!Start Testing !

Your !JavaScript!

Page 2: Stop Making Excuses and Start Testing Your JavaScript

• Senior UI Engineer at Netflix

[email protected]

• @bittersweeryan

About Me

Page 3: Stop Making Excuses and Start Testing Your JavaScript

Today’s Agenda

• Adding Testing to Your Project

• Solving Common Testing Issues

• Writing Testable JavaScript

• Automating Testing

Page 4: Stop Making Excuses and Start Testing Your JavaScript

Adding Testing To Your Project

Page 5: Stop Making Excuses and Start Testing Your JavaScript

Step 1: Pick Your Environment

Page 6: Stop Making Excuses and Start Testing Your JavaScript

TapeJest

Page 7: Stop Making Excuses and Start Testing Your JavaScript

Browser

Jest

Page 8: Stop Making Excuses and Start Testing Your JavaScript

Step 2: Pick Your Dialect

Page 9: Stop Making Excuses and Start Testing Your JavaScript

expectdescribe( 'adder', function(){ it( 'should return zero when no args...', function(){ expect( adder() ).to.equal( 0 ); } ); } );

+

Page 10: Stop Making Excuses and Start Testing Your JavaScript

shoulddescribe( 'adder', function(){ it( 'should return zero when no args...', function(){ adder().should.equal( 0 ); } ); } );

+

Page 11: Stop Making Excuses and Start Testing Your JavaScript

assert

+

//qunit test( 'adder', function(){ ok( adder() === 0, 'should return 0'); } ); !//chai describe( 'adder', function(){ assert.equal( adder() , 0, 'should return 0' ); });

Page 12: Stop Making Excuses and Start Testing Your JavaScript

Step 3: Setup

Page 13: Stop Making Excuses and Start Testing Your JavaScript
Page 14: Stop Making Excuses and Start Testing Your JavaScript

$> npm install -g mocha $> npm install mocha —save-dev $> npm install chai --save-dev

Page 15: Stop Making Excuses and Start Testing Your JavaScript

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

Page 16: Stop Making Excuses and Start Testing Your JavaScript

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

Page 17: Stop Making Excuses and Start Testing Your JavaScript

... describe( 'adder', function(){ it( 'should return zero if no args are passed in', function(){ expect( adder( ) ).to.equal(0); }); }) ...

adderSpec.js

Page 18: Stop Making Excuses and Start Testing Your JavaScript

... it( 'should skip non numbers', function(){ expect( adder( 1,2,'a' ) ).to.equal( 3 ); }); ...

adderSpec.js

Page 19: Stop Making Excuses and Start Testing Your JavaScript

$> mocha tests/spec

Name of your spec directory

Page 20: Stop Making Excuses and Start Testing Your JavaScript
Page 21: Stop Making Excuses and Start Testing Your JavaScript

Browser

Page 22: Stop Making Excuses and Start Testing Your JavaScript

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

Page 23: Stop Making Excuses and Start Testing Your JavaScript

Browser

Page 24: Stop Making Excuses and Start Testing Your JavaScript

Solving Common Testing Issues

Page 25: Stop Making Excuses and Start Testing Your JavaScript

I Have Methods that Call Other Methods.

Page 26: Stop Making Excuses and Start Testing Your JavaScript

Spies/Stubs/Mocks

Page 27: Stop Making Excuses and Start Testing Your JavaScript
Page 28: Stop Making Excuses and Start Testing Your JavaScript

SpiesUser.prototype.addUser = function(){ if(validateUser()){ saveUser(); } else{ addError( 'user not valid' ); } };

Page 29: Stop Making Excuses and Start Testing Your JavaScript

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

Page 30: Stop Making Excuses and Start Testing Your JavaScript

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

Page 31: Stop Making Excuses and Start Testing Your JavaScript

Spies That Return Data User.prototype.listUsers = function(){ var users = this.getUsers(); users.forEach( function( user ){ this.addUserToList( user ); }); };

Page 32: Stop Making Excuses and Start Testing Your JavaScript

Spies That Return Datait('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(); });

Page 33: Stop Making Excuses and Start Testing Your JavaScript

Promises

Page 34: Stop Making Excuses and Start Testing Your JavaScript

Returns A PromisegetStringValue: 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; }

Page 35: Stop Making Excuses and Start Testing Your JavaScript

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

Page 36: Stop Making Excuses and Start Testing Your JavaScript

I Need To Test the DOM

Page 37: Stop Making Excuses and Start Testing Your JavaScript

Do you REALLY need to?

Page 38: Stop Making Excuses and Start Testing Your JavaScript
Page 39: Stop Making Excuses and Start Testing Your JavaScript

Spy On The DOMfunction addItems( $el, items ){ items.forEach( function( item ){ $el.append( item ); } ); }

Page 40: Stop Making Excuses and Start Testing Your JavaScript

Spy On The DOMit( 'should add items', function(){ var $el = $( '<ul/>' ), items = [ { name : 'test' } ]; spyOn( jQuery.fn, 'append' ); ! addItems( $el, items ); expect( jQuery.fn.append ) .toHaveBeenCalled(); } );

Page 41: Stop Making Excuses and Start Testing Your JavaScript

Jasmine jQuery

https://github.com/velesin/jasmine-jquery

Create Fixtures as .html files

Load fixtures into a test

Make assertions

Page 42: Stop Making Excuses and Start Testing Your JavaScript

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

Page 43: Stop Making Excuses and Start Testing Your JavaScript

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

Page 44: Stop Making Excuses and Start Testing Your JavaScript

I Have to Test Async Code

Page 45: Stop Making Excuses and Start Testing Your JavaScript

Async Testfunction readConfig( callback ){ ! fs.readFile( config.fileLocation, callback ); }

Page 46: Stop Making Excuses and Start Testing Your JavaScript

Async Testit( 'should read the config file' , function( done ){ var callback = function( readystate ){ expect( readystate ) .to.equal( 'config ready' ); done(); } readConfig( callback ); } );

Page 47: Stop Making Excuses and Start Testing Your JavaScript

Async TestasyncTest( 'should read the config file' , function( ){ var callback = function( readystate ){ ok( readystate === 'config ready' ); start(); } readConfig( callback ); } );

Page 48: Stop Making Excuses and Start Testing Your JavaScript

Writing Testable JavaScript

Page 49: Stop Making Excuses and Start Testing Your 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

Page 50: Stop Making Excuses and Start Testing Your JavaScript

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

Page 51: Stop Making Excuses and Start Testing Your JavaScript

Make It A Constructorvar Todos = function( $list, $input ){ this.$list = $list; this.$input = $input; };

Page 52: Stop Making Excuses and Start Testing Your JavaScript

A Little Setup & First Testdescribe( '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' ); });

Page 53: Stop Making Excuses and Start Testing Your 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( '' ); } } );

Magic Data

Page 54: Stop Making Excuses and Start Testing Your JavaScript

getDataTodos.prototype.getData = function( success, error ){ $.ajax({ ... success : success, error : error ... }); }

Page 55: Stop Making Excuses and Start Testing Your JavaScript

Test getDatait(‘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!**

Page 56: Stop Making Excuses and Start Testing Your JavaScript

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

Page 57: Stop Making Excuses and Start Testing Your JavaScript

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

Page 58: Stop Making Excuses and Start Testing Your JavaScript

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

Page 59: Stop Making Excuses and Start Testing Your JavaScript

addItemTodos.prototype.addItem = function( name ){ this.$list.append( ’<li class="todo-item">' + name + ‘</li>' ); };

Page 60: Stop Making Excuses and Start Testing Your JavaScript

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

Page 61: Stop Making Excuses and Start Testing Your JavaScript

addItemsTodos.prototype.addItems = function( items ){ items.forEach( function( item, index){ this.addItem( item.name ); }, this ); };

Page 62: Stop Making Excuses and Start Testing Your JavaScript

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

Page 63: Stop Making Excuses and Start Testing Your 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( '' ); } } );

Anonymous Function

Page 64: Stop Making Excuses and Start Testing Your JavaScript

handleKeyPressTodos.prototype.handleKeypress = function( e ){ if( e.which === 13 ){ this.addItem( this.$input.val() ); this.$input.val( '' ); } };

Page 65: Stop Making Excuses and Start Testing Your JavaScript

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

Page 66: Stop Making Excuses and Start Testing Your JavaScript

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

Page 67: Stop Making Excuses and Start Testing Your JavaScript

Init functionTodos.prototype.init = function(){ this.$input.on( 'keyup', $.proxy( this.handleKeypress, this ) ); this.getData( this.addItems ); };

Page 68: Stop Making Excuses and Start Testing Your JavaScript

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

Page 69: Stop Making Excuses and Start Testing Your JavaScript

A Few More Tips

• Write maintainable tests • Avoid Singletons • Use named functions instead of anonymous

callbacks • Keep your functions small

Page 70: Stop Making Excuses and Start Testing Your JavaScript

Automating Testing

Page 71: Stop Making Excuses and Start Testing Your JavaScript

The key to successful testing is making writing and running tests

easy.

Page 72: Stop Making Excuses and Start Testing Your JavaScript

The key to successful testing is making writing and running tests

easy.

Page 73: Stop Making Excuses and Start Testing Your JavaScript

The key to successful testing is making writing and running tests

easy.

Page 74: Stop Making Excuses and Start Testing Your JavaScript

The key to successful testing is making writing and running tests

easy.

Page 75: Stop Making Excuses and Start Testing Your JavaScript

Using NPM

{ ... "scripts": { "test": "mocha tests/spec" }, ... }

Package.json

Page 76: Stop Making Excuses and Start Testing Your JavaScript

Running Build Tests

$> npm test

Page 77: Stop Making Excuses and Start Testing Your JavaScript

Using Your Build Tool

npm install -g grunt-cli

npm install grunt-contrib-jasmine

npm install grunt

Page 78: Stop Making Excuses and Start Testing Your JavaScript

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

Page 79: Stop Making Excuses and Start Testing Your JavaScript

Running Build Tests

$> grunt jasmine

Page 80: Stop Making Excuses and Start Testing Your JavaScript

Testem

npm install -g testem

Page 81: Stop Making Excuses and Start Testing Your JavaScript

testem.json{ "src_files" : [ "jquery.js", "todos-better.js", "tests/spec/todoSpec.js" ], "launch_in_dev" : [ "PhantomJS", “chrome”, “firefox” ], "framework" : "jasmine2" }

Page 82: Stop Making Excuses and Start Testing Your JavaScript

Testem

$> testem

Page 83: Stop Making Excuses and Start Testing Your JavaScript

Questions?

Page 84: Stop Making Excuses and Start Testing Your JavaScript

Thank You

[email protected]@bittersweetryan