Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Writing Testable JavaScript
Search
Rebecca Murphey
November 09, 2012
Technology
29
6k
Writing Testable JavaScript
Slides from my 2012 Full Frontal presentation.
Rebecca Murphey
November 09, 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
280
Making It Better Without Making It Over (Front Porch)
rmurphey
1
240
CascadiaJS: Making it Better Without Making it Over
rmurphey
2
200
Making it Better without Making it Over
rmurphey
1
250
Making It Better Without Making It Over
rmurphey
0
360
HTTP/2 is here, now let's make it easy
rmurphey
7
8.9k
Deploying client-side apps, 1000 (or so) at a time
rmurphey
1
400
Apps That Talk Back (Codementor)
rmurphey
1
6.4k
Ain't No Party Like a Third-Party JS Party
rmurphey
1
310
Other Decks in Technology
See All in Technology
Agile Leadership Summit Keynote 2026
m_seki
1
410
Context Engineeringの取り組み
nutslove
0
280
SREのプラクティスを用いた3領域同時 マネジメントへの挑戦 〜SRE・情シス・セキュリティを統合した チーム運営術〜
coconala_engineer
2
590
MCPでつなぐElasticsearchとLLM - 深夜の障害対応を楽にしたい / Bridging Elasticsearch and LLMs with MCP
sashimimochi
0
140
GitLab Duo Agent Platform × AGENTS.md で実現するSpec-Driven Development / GitLab Duo Agent Platform × AGENTS.md
n11sh1
0
120
Sansan Engineering Unit 紹介資料
sansan33
PRO
1
3.8k
入社1ヶ月でデータパイプライン講座を作った話
waiwai2111
1
240
Meshy Proプラン課金した
henjin0
0
250
サイボウズ 開発本部採用ピッチ / Cybozu Engineer Recruit
cybozuinsideout
PRO
10
73k
Digitization部 紹介資料
sansan33
PRO
1
6.8k
日本の85%が使う公共SaaSは、どう育ったのか
taketakekaho
1
140
What happened to RubyGems and what can we learn?
mikemcquaid
0
240
Featured
See All Featured
Stop Working from a Prison Cell
hatefulcrawdad
273
21k
Chrome DevTools: State of the Union 2024 - Debugging React & Beyond
addyosmani
10
1.1k
Marketing to machines
jonoalderson
1
4.6k
The Illustrated Guide to Node.js - THAT Conference 2024
reverentgeek
0
250
The Limits of Empathy - UXLibs8
cassininazir
1
210
The untapped power of vector embeddings
frankvandijk
1
1.6k
KATA
mclloyd
PRO
34
15k
CoffeeScript is Beautiful & I Never Want to Write Plain JavaScript Again
sstephenson
162
16k
Kristin Tynski - Automating Marketing Tasks With AI
techseoconnect
PRO
0
130
Google's AI Overviews - The New Search
badams
0
900
Leveraging LLMs for student feedback in introductory data science courses - posit::conf(2025)
minecr
0
140
Exploring anti-patterns in Rails
aemeredith
2
250
Transcript
Writing Testable JavaScript Rebecca Murphey • Full Frontal 2012 ©
brianUphoto http://www.flickr.com/photos/snype451/5752753663/in/photostream/ Friday, November 9, 12
rmurphey.com • @rmurphey • bocoup.com Friday, November 9, 12
Writing Testable JavaScript Rebecca Murphey • Full Frontal 2012 ©
brianUphoto http://www.flickr.com/photos/snype451/5752753663/in/photostream/ Friday, November 9, 12
Friday, November 9, 12
Friday, November 9, 12
Friday, November 9, 12
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 Friday, November 9, 12
Friday, November 9, 12
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() ); }); Friday, November 9, 12
“do the pieces work together as expected?” integration tests Friday,
November 9, 12
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) Friday, November 9, 12
Friday, November 9, 12
Friday, November 9, 12
“given input x, is the output y?” unit tests Friday,
November 9, 12
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() ); }); Friday, November 9, 12
anonymous functions, lack of structure complex, oversized functions lack of
con gurability hidden or shared state tightly coupled difficult to test Friday, November 9, 12
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 ) { // ... }); Friday, November 9, 12
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; } }); Friday, November 9, 12
Friday, November 9, 12
setup presentation & interaction application state data/server communication Friday,
November 9, 12
Friday, November 9, 12
search data application state glue Friday, November 9, 12
use constructors to create instances support con gurability keep methods
simple don’t intermingle responsibilities guiding principles Friday, November 9, 12
test rst. Friday, November 9, 12
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 ); }); Friday, November 9, 12
test('create a likes view', function() { var view =
new app.Views.Likes({ el: '#likes' }); ok( view ); }); Friday, November 9, 12
test('add a liked person', function() { var view =
new app.Views.Likes({ el: '#likes' }); view.add( 'Brendan Eich' ); ok( $('#likes').html().match('Brendan Eich') ); }); Friday, November 9, 12
app.Views.Likes = (function() { var Likes = function( settings
) { this.$el = $( settings.el ); }; Likes.prototype.add = function( name ) { this.$el.find( '.no-‐results' ).remove(); $( '<li>', { html : name } ).appendTo( this.$el ); }; return Likes; }()); Friday, November 9, 12
$.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; } }); Friday, November 9, 12
test('adding search results', function() { var view = new
app.Views.SearchResults({ el: '#results' }); view.setData([ { name: 'Rebecca', company: { name: 'Bocoup' }, email: 'rebecca@ { name: 'Dan', company: { name: 'Bocoup' }, email: '
[email protected]
]); var html = $( '#results' ).html(); ok( html.match( 'Rebecca' ) ); ok( html.match( 'Dan' ) ); equal( $( '#results li' ).length, 2 ); }); Friday, November 9, 12
app.Views.SearchResults = (function() { var SearchResults = function( settings
) { this.app = settings.app; this.$el = $( settings.el ); }; SearchResults.prototype.set = function( people ) { var $el = this.$el.empty(); return app.loadTemplate( 'people-‐detailed.tmpl' ) .done(function( t ) { $el.html( t( { people : people } ) ); }); }; return SearchResults; }()); Friday, November 9, 12
test('search data URL', function() { var search = new
app.Search(); var result = search.fetch( 'cat' ); equal( requests[0].url, '/data/search.json?q=cat' ); }); Friday, November 9, 12
module('search data', { setup : function() {
xhr = sinon.useFakeXMLHttpRequest(); requests = []; xhr.onCreate = function( req ) { requests.push( req ); }; }, teardown : function() { xhr.restore(); } }); Friday, November 9, 12
test('fetch returns a promise', function() { var search =
new app.Search(); var result = search.fetch( 'cat' ); ok( result.then ); }); Friday, November 9, 12
test('promise resolves with array', 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 ) { equal( data[0], 'cat' ); }); }); Friday, November 9, 12
app.Search = (function() { var Search = function() {};
var processResults = function( resp ) { return resp.results; }; Search.prototype.fetch = function( query ) { return $.getJSON( '/data/search.json', { q : query }).pipe( processResults ); }; return Search; }()); Friday, November 9, 12
so. Friday, November 9, 12
where to start? Friday, November 9, 12
awesome tools grunt + qunit + phantomjs grunt-mocha grunt-jasmine sinon.js
chai.js Friday, November 9, 12
$ grunt init:gruntfile Friday, November 9, 12
Friday, November 9, 12
/*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'] }, qunit: { files: ['test/**/test-‐*.html'] }, 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: {} }); grunt.registerTask('default', 'lint qunit'); }; Friday, November 9, 12
/*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'] }, qunit: { files: ['test/**/test-‐*.html'] }, 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: {} }); grunt.registerTask('default', 'lint qunit'); }; qunit: { files: ['test/**/test-‐*.html'] }, Friday, November 9, 12
Friday, November 9, 12
Friday, November 9, 12
pinboard.in/u:rmurphey/t:testable-‐javascript/ bit.ly/WL0R8U Friday, November 9, 12
rmurphey.com • @rmurphey • bocoup.com Friday, November 9, 12