Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Speaker Deck
PRO
Sign in
Sign up for free
Writing Testable JavaScript (Mocha Version)
Rebecca Murphey
November 13, 2012
Technology
61
93k
Writing Testable JavaScript (Mocha Version)
Rebecca Murphey
November 13, 2012
Tweet
Share
More Decks by Rebecca Murphey
See All by Rebecca Murphey
You Can't Always Get What You Want: A Year of Leading Change
rmurphey
0
200
Making It Better Without Making It Over (Front Porch)
rmurphey
1
180
CascadiaJS: Making it Better Without Making it Over
rmurphey
2
130
Making it Better without Making it Over
rmurphey
1
200
Making It Better Without Making It Over
rmurphey
0
300
HTTP/2 is here, now let's make it easy
rmurphey
7
8.5k
Deploying client-side apps, 1000 (or so) at a time
rmurphey
1
320
Apps That Talk Back (Codementor)
rmurphey
1
6.3k
Ain't No Party Like a Third-Party JS Party
rmurphey
1
250
Other Decks in Technology
See All in Technology
初めてのデータ移行プロジェクトから得た学び
tjmtmmnk
0
220
NGINXENG JP#2 - 1-NGINX-エンジニアリング勉強会-きょうの見どころ
hiropo20
0
100
Pentesting Password Reset Functionality
anugrahsr
0
440
OCI技術資料 : ロード・バランサー 詳細 / Load Balancer 200
ocise
2
7.2k
re:Invent2022 前後の Amazon EventBridge のアップデートを踏まえつつ、情シスの仕事をより楽しくしたい話。 / EventBridge for Information Systems Department
_kensh
2
700
Oracle Cloud Infrastructure:2023年1月度サービス・アップデート
oracle4engineer
PRO
0
150
OpenShiftのリリースノートを整理してみた
loftkun
2
320
OpenShiftでスポットVMを使おう.pdf
jpishikawa
1
280
OCI DevOps 概要 / OCI DevOps overview
oracle4engineer
PRO
0
490
経営統合をきっかけに会社をエンジニアリングした話 / btconjp-2023
carta_engineering
0
150
PCI DSS に準拠したシステム開発
yutadayo
0
310
【NGK2023S】 ノードエディタ形式の画像処理ツール「Image-Processing-Node-Editor」
kazuhitotakahashi
0
270
Featured
See All Featured
What’s in a name? Adding method to the madness
productmarketing
12
1.9k
Sharpening the Axe: The Primacy of Toolmaking
bcantrill
7
570
Refactoring Trust on Your Teams (GOTO; Chicago 2020)
rmw
22
1.7k
Designing with Data
zakiwarfel
91
4.2k
Debugging Ruby Performance
tmm1
67
11k
Fantastic passwords and where to find them - at NoRuKo
philnash
32
1.8k
For a Future-Friendly Web
brad_frost
166
7.8k
What's new in Ruby 2.0
geeforr
336
30k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
351
21k
Web Components: a chance to create the future
zenorocha
304
40k
StorybookのUI Testing Handbookを読んだ
zakiyama
8
3.2k
Reflections from 52 weeks, 52 projects
jeffersonlam
338
18k
Transcript
Writing Testable JavaScript Rebecca Murphey • JS Summit 2012 ©
brianUphoto http://www.flickr.com/photos/snype451/5752753663/in/photostream/
rmurphey.com • @rmurphey • bocoup.com
Writing Testable JavaScript Rebecca Murphey • JS Summit 2012 ©
brianUphoto http://www.flickr.com/photos/snype451/5752753663/in/photostream/
None
None
None
you will design your code you will test your code
you will refactor your code other people will use your code there will be bugs facts of life
None
var resultsList = $( '#results' ); var liked = $(
'#liked' ); var pending = false; $( '#searchForm' ).on( 'submit', function( e ) { e.preventDefault(); if ( pending ) { return; } var form = $( this ); var query = $.trim( form.find( 'input[name="q"]' ).val() ); if ( !query ) { return; } pending = true; $.ajax( '/data/search.json', { data : { q: query }, dataType : 'json', success : function( data ) { var tmpl = _.template( $('#tmpl-‐people-‐detailed').text() ); resultsList.html(tmpl({ people : data.results })); pending = false; } }); $('<li>', { 'class' : 'pending', html : 'Searching …' }).appendTo( resultsList.empty() ); });
“do the pieces work together as expected?” integration tests
def test_no_results fill_in('q', :with => 'foobarbazbimbop') find('.btn').click
assert( page.has_selector?('#results li.no-‐results'), 'No results is shown' ) assert( find('#results').has_content?('No results found'), 'No results message is shown' ) end selenium (via ruby)
None
“given input x, is the output y?” unit tests
var resultsList = $( '#results' ); var liked = $(
'#liked' ); var pending = false; $( '#searchForm' ).on( 'submit', function( e ) { e.preventDefault(); if ( pending ) { return; } var form = $( this ); var query = $.trim( form.find( 'input[name="q"]' ).val() ); if ( !query ) { return; } pending = true; $.ajax( '/data/search.json', { data : { q: query }, dataType : 'json', success : function( data ) { var tmpl = _.template( $('#tmpl-‐people-‐detailed').text() ); resultsList.html(tmpl({ people : data.results })); pending = false; } }); $('<li>', { 'class' : 'pending', html : 'Searching …' }).appendTo( resultsList.empty() ); });
anonymous functions, lack of structure complex, oversized functions lack of
con gurability hidden or shared state tightly coupled difficult to test
var resultsList = $( '#results' ); var liked = $(
'#liked' ); var pending = false; $( '#searchForm' ).on( 'submit', function( e ) { e.preventDefault(); if ( pending ) { return; } var form = $( this ); var query = $.trim( form.find( 'input[name="q"]' ).val() ); if ( !query ) { return; } pending = true; $.ajax( '/data/search.json', { data : { q: query }, dataType : 'json', success : function( data ) { var tmpl = _.template( $('#tmpl-‐people-‐detailed').text() ); resultsList.html(tmpl({ people : data.results })); pending = false; } }); $('<li>', { 'class' : 'pending', html : 'Searching …' }).appendTo( resultsList.empty() ); }); var resultsList = $( '#results' ); var liked = $( '#liked' ); var pending = false; $( '#searchForm' ).on( 'submit', function( e ) { // ... });
var resultsList = $( '#results' ); var liked = $(
'#liked' ); var pending = false; $( '#searchForm' ).on( 'submit', function( e ) { e.preventDefault(); if ( pending ) { return; } var form = $( this ); var query = $.trim( form.find( 'input[name="q"]' ).val() ); if ( !query ) { return; } pending = true; $.ajax( '/data/search.json', { data : { q: query }, dataType : 'json', success : function( data ) { var tmpl = _.template( $('#tmpl-‐people-‐detailed').text() ); resultsList.html(tmpl({ people : data.results })); pending = false; } }); $('<li>', { 'class' : 'pending', html : 'Searching …' }).appendTo( resultsList.empty() ); }); $.ajax( '/data/search.json', { data : { q: query }, dataType : 'json', success : function( data ) { var tmpl = _.template( $('#tmpl-‐people-‐detailed').text() ); resultsList.html(tmpl({ people : data.results })); pending = false; } });
None
setup presentation & interaction application state data/server communication
None
search data application state glue
use constructors to create instances support con gurability keep methods
simple don’t intermingle responsibilities guiding principles
test rst.
var liked = $( '#liked' ); var resultsList = $(
'#results' ); // ... resultsList.on( 'click', '.like', function(e) { e.preventDefault(); var name = $( this ).closest( 'li' ).find( 'h2' ).text(); liked.find( '.no-‐results' ).remove(); $( '<li>', { text: name } ).appendTo( liked ); });
test('likes constructor', function() { var likes = new app.Likes({
el : '#likes' }); assert( likes ); });
test('likes are properly displayed', function() { var likes =
new app.Likes({ el : '#likes' }); likes.add( 'Brendan Eich' ); assert( $( '#likes' ).html().match( 'Brendan Eich' ), 'Like is added' ); });
app.Likes = (function() { var Likes = function( settings
) { if (! (this instanceof Likes) ) { return new Likes( settings ); } this.$el = $( settings.el ); }; Likes.prototype.add = function( name ) { this.$el.find( '.no-‐results' ).remove(); $( '<li>', { html : name } ).appendTo( this.$el ); }; return Likes; }());
$.ajax( '/data/search.json', { data : { q: query },
dataType : 'json', success : function( data ) { var tmpl = _.template( $('#tmpl-‐people-‐detailed').text() ); resultsList.html( tmpl({ people : data.results }) ); pending = false; } });
test('results are displayed', function( done ) { var results
= new app.SearchResults({ el : '#results' }); results.set( [ { name: 'Rebecca', company: { name: 'Bocoup' }, email: 'rebecca { name: 'Dan', company: { name: 'Bocoup' }, email: '
[email protected]
] ); assert.equal( $( '#results li' ).length, 2 ); assert( ! $( '#results li.no-‐results' ).length ); assert( $( '#results' ).html().match('Dan') ); assert( $( '#results' ).html().match('Rebecca') ); });
app.SearchResults = (function() { var SearchResults = function( settings
) { if (! (this instanceof SearchResults) ) { return new SearchResults( settings ); } this.$el = $( settings.el ); }; SearchResults.prototype.set = function( people ) { var $el = this.$el.empty(); return app.loadTemplate( 'people-‐detailed.tmpl' ).done(function( t ) { var html = t( { people : people } ); $el.html( html ); }); }; return SearchResults; }());
test('data is fetched from the right URL', function() {
var search = new app.Search(); var result = search.fetch('cat'); assert.equal( requests[0].url, '/data/search.json?q=cat' ); });
suite('search data', function() { var xhr, requests; setup(function()
{ xhr = sinon.useFakeXMLHttpRequest(); requests = []; xhr.onCreate = function( req ) { requests.push(req); }; }); teardown(function() { xhr.restore(); }); // ... });
test('fetch returns a promise', function() { var search =
new app.Search(); var result = search.fetch('cat'); assert( result.then, 'result is a promise' ); });
test('promise resolves with array of data', function() { var
search = new app.Search(); var result = search.fetch('cat'); requests[0].respond( 200, { "Content-‐type" : "text/json" }, JSON.stringify( { results : [ 'cat' ] } ) ); result.done(function( data ) { assert.equal( data[0], 'cat' ); }); });
app.Search = (function() { var Search = function() {
if (! (this instanceof Search) ) { return new Search(); } }; var processResults = function( resp ) { return resp.results; }; Search.prototype.fetch = function( query ) { return $.getJSON( '/data/search.json', { q : query }).pipe( processResults ); }; return Search; }());
so.
where to start?
awesome tools grunt + qunit + phantomjs grunt-mocha grunt-jasmine sinon.js
chai.js
$ grunt init:gruntfile
None
/*global module:false*/ var child_process = require('child_process'); module.exports = function(grunt) {
// Project configuration. grunt.initConfig({ lint: { files: ['lib/**/*.js', 'test/**/*.js', '! test/lib/**/*.js', 'www/js/**/*.js'] }, watch: { files: [ '<config:lint.files>', 'www/templates/*.tmpl' ], tasks: 'test' }, jshint: { options: { curly: true, eqeqeq: true, immed: true, latedef: true, newcap: true, noarg: true, sub: true, undef: true, boss: true, eqnull: true, browser: true }, globals: { $ : true, _ : true, RSVP : true, app : true } }, uglify: {}, mocha: { index: [ 'test/runner/index.html' ] } }); grunt.registerTask('build-‐template-‐mocks', 'Build template mocks', function() { var obj = {}; var addFile = function(filepath, contents) { obj[ filepath.replace('templates/', '') ] = contents; }; var options = {cwd: 'www'}; grunt.file.expand(options, 'templates/*.tmpl').forEach(function(filepath) { addFile(filepath, grunt.file.read('www/' + filepath)); }); var src = 'window._templateMocks = ' + JSON.stringify(obj, null, 2) + ';'; grunt.file.write('test/fixtures/templates.js', src); }); grunt.loadNpmTasks('grunt-‐mocha'); grunt.registerTask('test', 'build-‐template-‐mocks mocha'); grunt.registerTask('default', 'lint test'); };
/*global module:false*/ var child_process = require('child_process'); module.exports = function(grunt) {
// Project configuration. grunt.initConfig({ lint: { files: ['lib/**/*.js', 'test/**/*.js', '! test/lib/**/*.js', 'www/js/**/*.js'] }, watch: { files: [ '<config:lint.files>', 'www/templates/*.tmpl' ], tasks: 'test' }, jshint: { options: { curly: true, eqeqeq: true, immed: true, latedef: true, newcap: true, noarg: true, sub: true, undef: true, boss: true, eqnull: true, browser: true }, globals: { $ : true, _ : true, RSVP : true, app : true } }, uglify: {}, mocha: { index: [ 'test/runner/index.html' ] } }); grunt.registerTask('build-‐template-‐mocks', 'Build template mocks', function() { var obj = {}; var addFile = function(filepath, contents) { obj[ filepath.replace('templates/', '') ] = contents; }; var options = {cwd: 'www'}; grunt.file.expand(options, 'templates/*.tmpl').forEach(function(filepath) { addFile(filepath, grunt.file.read('www/' + filepath)); }); var src = 'window._templateMocks = ' + JSON.stringify(obj, null, 2) + ';'; grunt.file.write('test/fixtures/templates.js', src); }); grunt.loadNpmTasks('grunt-‐mocha'); grunt.registerTask('test', 'build-‐template-‐mocks mocha'); grunt.registerTask('default', 'lint test'); }; mocha: { index: [ 'test/runner/index.html' ] }
None
None
pinboard.in/u:rmurphey/t:testable-‐ javascript/
rmurphey.com • @rmurphey • bocoup.com