Writing Testable JavaScript (Mocha Version)

Writing Testable JavaScript (Mocha Version)

0177cdce6af15e10db15b6bf5dc4e0b0?s=128

Rebecca Murphey

November 13, 2012
Tweet

Transcript

  1. Writing Testable JavaScript Rebecca Murphey • JS Summit 2012 ©

     brianUphoto  http://www.flickr.com/photos/snype451/5752753663/in/photostream/
  2. rmurphey.com • @rmurphey • bocoup.com

  3. Writing Testable JavaScript Rebecca Murphey • JS Summit 2012 ©

     brianUphoto  http://www.flickr.com/photos/snype451/5752753663/in/photostream/
  4. None
  5. None
  6. None
  7. 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
  8. None
  9. 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  &hellip;'    }).appendTo(  resultsList.empty()  ); });
  10. “do the pieces work together as expected?” integration tests

  11. 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)
  12. None
  13. “given input x, is the output y?” unit tests

  14. 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  &hellip;'    }).appendTo(  resultsList.empty()  ); });
  15. anonymous functions, lack of structure complex, oversized functions lack of

    con gurability hidden or shared state tightly coupled difficult to test
  16. 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  &hellip;'    }).appendTo(  resultsList.empty()  ); }); var  resultsList  =  $(  '#results'  ); var  liked  =  $(  '#liked'  ); var  pending  =  false; $(  '#searchForm'  ).on(  'submit',  function(  e  )  {    //  ... });
  17. 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  &hellip;'    }).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;    } });
  18. None
  19. setup presentation &  interaction application state data/server   communication

  20. None
  21. search data application state glue

  22. use constructors to create instances support con gurability keep methods

    simple don’t intermingle responsibilities guiding principles
  23. test rst.

  24. 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  ); });
  25. test('likes  constructor',  function()  {    var  likes  =  new  app.Likes({

     el  :  '#likes'  });    assert(  likes  ); });
  26. 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'      ); });
  27. 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; }());
  28. $.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;    } });
  29. 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')  ); });
  30. 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; }());
  31. 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'      ); });
  32. suite('search  data',  function()  {    var  xhr,  requests;    setup(function()

     {        xhr  =  sinon.useFakeXMLHttpRequest();        requests  =  [];        xhr.onCreate  =  function(  req  )  {              requests.push(req);          };    });    teardown(function()  {        xhr.restore();    });        //  ... });
  33. test('fetch  returns  a  promise',  function()  {    var  search  =

     new  app.Search();    var  result  =  search.fetch('cat');    assert(  result.then,  'result  is  a  promise'  ); });
  34. 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'  );    }); });
  35. 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; }());
  36. so.

  37. where to start?

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

    chai.js
  39. $  grunt  init:gruntfile

  40. None
  41. /*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'); };
  42. /*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'  ] }
  43. None
  44. None
  45. pinboard.in/u:rmurphey/t:testable-­‐ javascript/

  46. rmurphey.com • @rmurphey • bocoup.com