Slide 1

Slide 1 text

Writing Testable JavaScript Rebecca Murphey • JS Summit 2012 ©  brianUphoto  http://www.flickr.com/photos/snype451/5752753663/in/photostream/

Slide 2

Slide 2 text

rmurphey.com • @rmurphey • bocoup.com

Slide 3

Slide 3 text

Writing Testable JavaScript Rebecca Murphey • JS Summit 2012 ©  brianUphoto  http://www.flickr.com/photos/snype451/5752753663/in/photostream/

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

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;        }    });    $('
  • ',  {        'class'  :  'pending',        html  :  'Searching  …'    }).appendTo(  resultsList.empty()  ); });
  • Slide 10

    Slide 10 text

    “do the pieces work together as expected?” integration tests

    Slide 11

    Slide 11 text

    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)

    Slide 12

    Slide 12 text

    No content

    Slide 13

    Slide 13 text

    “given input x, is the output y?” unit tests

    Slide 14

    Slide 14 text

    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;        }    });    $('
  • ',  {        'class'  :  'pending',        html  :  'Searching  …'    }).appendTo(  resultsList.empty()  ); });
  • Slide 15

    Slide 15 text

    anonymous functions, lack of structure complex, oversized functions lack of con gurability hidden or shared state tightly coupled difficult to test

    Slide 16

    Slide 16 text

    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;        }    });    $('
  • ',  {        'class'  :  'pending',        html  :  'Searching  …'    }).appendTo(  resultsList.empty()  ); }); var  resultsList  =  $(  '#results'  ); var  liked  =  $(  '#liked'  ); var  pending  =  false; $(  '#searchForm'  ).on(  'submit',  function(  e  )  {    //  ... });
  • Slide 17

    Slide 17 text

    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;        }    });    $('
  • ',  {        '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;    } });
  • Slide 18

    Slide 18 text

    No content

    Slide 19

    Slide 19 text

    setup presentation &  interaction application state data/server   communication

    Slide 20

    Slide 20 text

    No content

    Slide 21

    Slide 21 text

    search data application state glue

    Slide 22

    Slide 22 text

    use constructors to create instances support con gurability keep methods simple don’t intermingle responsibilities guiding principles

    Slide 23

    Slide 23 text

    test rst.

    Slide 24

    Slide 24 text

    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();    $(  '
  • ',  {  text:  name  }  ).appendTo(  liked  ); });
  • Slide 25

    Slide 25 text

    test('likes  constructor',  function()  {    var  likes  =  new  app.Likes({  el  :  '#likes'  });    assert(  likes  ); });

    Slide 26

    Slide 26 text

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

    Slide 27

    Slide 27 text

    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();        $(  '
  • ',  {  html  :  name  }  ).appendTo(  this.$el  );    };    return  Likes; }());
  • Slide 28

    Slide 28 text

    $.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;    } });

    Slide 29

    Slide 29 text

    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:  'dan@bocoup.    ]  );    assert.equal(  $(  '#results  li'  ).length,  2  );    assert(  !  $(  '#results  li.no-­‐results'  ).length  );    assert(  $(  '#results'  ).html().match('Dan')  );    assert(  $(  '#results'  ).html().match('Rebecca')  ); });

    Slide 30

    Slide 30 text

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

    Slide 31

    Slide 31 text

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

    Slide 32

    Slide 32 text

    suite('search  data',  function()  {    var  xhr,  requests;    setup(function()  {        xhr  =  sinon.useFakeXMLHttpRequest();        requests  =  [];        xhr.onCreate  =  function(  req  )  {              requests.push(req);          };    });    teardown(function()  {        xhr.restore();    });        //  ... });

    Slide 33

    Slide 33 text

    test('fetch  returns  a  promise',  function()  {    var  search  =  new  app.Search();    var  result  =  search.fetch('cat');    assert(  result.then,  'result  is  a  promise'  ); });

    Slide 34

    Slide 34 text

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

    Slide 35

    Slide 35 text

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

    Slide 36

    Slide 36 text

    so.

    Slide 37

    Slide 37 text

    where to start?

    Slide 38

    Slide 38 text

    awesome tools grunt + qunit + phantomjs grunt-mocha grunt-jasmine sinon.js chai.js

    Slide 39

    Slide 39 text

    $  grunt  init:gruntfile

    Slide 40

    Slide 40 text

    No content

    Slide 41

    Slide 41 text

    /*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:  [  '',  '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'); };

    Slide 42

    Slide 42 text

    /*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:  [  '',  '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'  ] }

    Slide 43

    Slide 43 text

    No content

    Slide 44

    Slide 44 text

    No content

    Slide 45

    Slide 45 text

    pinboard.in/u:rmurphey/t:testable-­‐ javascript/

    Slide 46

    Slide 46 text

    rmurphey.com • @rmurphey • bocoup.com