Lock in $30 Savings on PRO—Offer Ends Soon! ⏳
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
270
Making It Better Without Making It Over (Front Porch)
rmurphey
1
230
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
390
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
業務の煩悩を祓うAI活用術108選 / AI 108 Usages
smartbank
9
12k
ActiveJobUpdates
igaiga
1
320
Authlete で実装する MCP OAuth 認可サーバー #CIMD の実装を添えて
watahani
0
180
投資戦略を量産せよ 2 - マケデコセミナー(2025/12/26)
gamella
0
450
会社紹介資料 / Sansan Company Profile
sansan33
PRO
11
390k
20251203_AIxIoTビジネス共創ラボ_第4回勉強会_BP山崎.pdf
iotcomjpadmin
0
140
Lookerで実現するセキュアな外部データ提供
zozotech
PRO
0
200
AI駆動開発ライフサイクル(AI-DLC)の始め方
ryansbcho79
0
190
モダンデータスタックの理想と現実の間で~1.3億人Vポイントデータ基盤の現在地とこれから~
taromatsui_cccmkhd
2
270
TED_modeki_共創ラボ_20251203.pdf
iotcomjpadmin
0
150
ハッカソンから社内プロダクトへ AIエージェント ko☆shi 開発で学んだ4つの重要要素
leveragestech
0
210
AI時代のワークフロー設計〜Durable Functions / Step Functions / Strands Agents を添えて〜
yakumo
3
2.3k
Featured
See All Featured
Unlocking the hidden potential of vector embeddings in international SEO
frankvandijk
0
130
Navigating Weather and Climate Data
rabernat
0
53
Visualization
eitanlees
150
16k
How to build an LLM SEO readiness audit: a practical framework
nmsamuel
1
580
[RailsConf 2023] Rails as a piece of cake
palkan
58
6.2k
Automating Front-end Workflow
addyosmani
1371
200k
Building AI with AI
inesmontani
PRO
1
580
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
37
2.7k
Evolution of real-time – Irina Nazarova, EuRuKo, 2024
irinanazarova
9
1.1k
Claude Code のすすめ
schroneko
67
210k
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
9.1k
Deep Space Network (abreviated)
tonyrice
0
22
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