Slide 1

Slide 1 text

Writing Testable JavaScript Rebecca Murphey • Full Frontal 2012 ©  brianUphoto  http://www.flickr.com/photos/snype451/5752753663/in/photostream/ Friday, November 9, 12

Slide 2

Slide 2 text

rmurphey.com • @rmurphey • bocoup.com Friday, November 9, 12

Slide 3

Slide 3 text

Writing Testable JavaScript Rebecca Murphey • Full Frontal 2012 ©  brianUphoto  http://www.flickr.com/photos/snype451/5752753663/in/photostream/ Friday, November 9, 12

Slide 4

Slide 4 text

Friday, November 9, 12

Slide 5

Slide 5 text

Friday, November 9, 12

Slide 6

Slide 6 text

Friday, November 9, 12

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 Friday, November 9, 12

Slide 8

Slide 8 text

Friday, November 9, 12

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()  ); }); Friday, November 9, 12
  • Slide 10

    Slide 10 text

    “do the pieces work together as expected?” integration tests Friday, November 9, 12

    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) Friday, November 9, 12

    Slide 12

    Slide 12 text

    Friday, November 9, 12

    Slide 13

    Slide 13 text

    Friday, November 9, 12

    Slide 14

    Slide 14 text

    “given input x, is the output y?” unit tests Friday, November 9, 12

    Slide 15

    Slide 15 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()  ); }); Friday, November 9, 12
  • Slide 16

    Slide 16 text

    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

    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()  ); }); var  resultsList  =  $(  '#results'  ); var  liked  =  $(  '#liked'  ); var  pending  =  false; $(  '#searchForm'  ).on(  'submit',  function(  e  )  {    //  ... }); Friday, November 9, 12
  • Slide 18

    Slide 18 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;    } }); Friday, November 9, 12
  • Slide 19

    Slide 19 text

    Friday, November 9, 12

    Slide 20

    Slide 20 text

    setup presentation &  interaction application state data/server   communication Friday, November 9, 12

    Slide 21

    Slide 21 text

    Friday, November 9, 12

    Slide 22

    Slide 22 text

    search data application state glue Friday, November 9, 12

    Slide 23

    Slide 23 text

    use constructors to create instances support con gurability keep methods simple don’t intermingle responsibilities guiding principles Friday, November 9, 12

    Slide 24

    Slide 24 text

    test rst. Friday, November 9, 12

    Slide 25

    Slide 25 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  ); }); Friday, November 9, 12
  • Slide 26

    Slide 26 text

    test('create  a  likes  view',  function()  {    var  view  =  new  app.Views.Likes({        el:  '#likes'    });        ok(  view  ); }); Friday, November 9, 12

    Slide 27

    Slide 27 text

    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

    Slide 28

    Slide 28 text

    app.Views.Likes  =  (function()  {    var  Likes  =  function(  settings  )  {        this.$el  =  $(  settings.el  );    };    Likes.prototype.add  =  function(  name  )  {        this.$el.find(  '.no-­‐results'  ).remove();        $(  '
  • ',  {  html  :  name  }  ).appendTo(  this.$el  );    };    return  Likes; }()); Friday, November 9, 12
  • Slide 29

    Slide 29 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;    } }); Friday, November 9, 12

    Slide 30

    Slide 30 text

    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

    Slide 31

    Slide 31 text

    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

    Slide 32

    Slide 32 text

    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

    Slide 33

    Slide 33 text

    module('search  data',  {    setup  :  function()  {        xhr  =  sinon.useFakeXMLHttpRequest();        requests  =  [];        xhr.onCreate  =  function(  req  )  {              requests.push(  req  );          };    },    teardown  :  function()  {        xhr.restore();    } }); Friday, November 9, 12

    Slide 34

    Slide 34 text

    test('fetch  returns  a  promise',  function()  {    var  search  =  new  app.Search();    var  result  =  search.fetch(  'cat'  );    ok(  result.then  ); }); Friday, November 9, 12

    Slide 35

    Slide 35 text

    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

    Slide 36

    Slide 36 text

    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

    Slide 37

    Slide 37 text

    so. Friday, November 9, 12

    Slide 38

    Slide 38 text

    where to start? Friday, November 9, 12

    Slide 39

    Slide 39 text

    awesome tools grunt + qunit + phantomjs grunt-mocha grunt-jasmine sinon.js chai.js Friday, November 9, 12

    Slide 40

    Slide 40 text

    $  grunt  init:gruntfile Friday, November 9, 12

    Slide 41

    Slide 41 text

    Friday, November 9, 12

    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']        },        qunit:  {            files:  ['test/**/test-­‐*.html']        },        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:  {}    });    grunt.registerTask('default',  'lint  qunit'); }; Friday, November 9, 12

    Slide 43

    Slide 43 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']        },        qunit:  {            files:  ['test/**/test-­‐*.html']        },        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:  {}    });    grunt.registerTask('default',  'lint  qunit'); }; qunit:  {    files:  ['test/**/test-­‐*.html'] }, Friday, November 9, 12

    Slide 44

    Slide 44 text

    Friday, November 9, 12

    Slide 45

    Slide 45 text

    Friday, November 9, 12

    Slide 46

    Slide 46 text

    pinboard.in/u:rmurphey/t:testable-­‐javascript/ bit.ly/WL0R8U Friday, November 9, 12

    Slide 47

    Slide 47 text

    rmurphey.com • @rmurphey • bocoup.com Friday, November 9, 12