Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing Testable JavaScript (Mocha Version)

Writing Testable JavaScript (Mocha Version)

Rebecca Murphey

November 13, 2012
Tweet

More Decks by Rebecca Murphey

Other Decks in Technology

Transcript

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

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

     brianUphoto  http://www.flickr.com/photos/snype451/5752753663/in/photostream/
  3. 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
  4. 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()  ); });
  5. 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)
  6. 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()  ); });
  7. anonymous functions, lack of structure complex, oversized functions lack of

    con gurability hidden or shared state tightly coupled difficult to test
  8. 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  )  {    //  ... });
  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()  ); }); $.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;    } });
  10. use constructors to create instances support con gurability keep methods

    simple don’t intermingle responsibilities guiding principles
  11. 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  ); });
  12. test('likes  constructor',  function()  {    var  likes  =  new  app.Likes({

     el  :  '#likes'  });    assert(  likes  ); });
  13. 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'      ); });
  14. 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; }());
  15. $.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;    } });
  16. 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')  ); });
  17. 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; }());
  18. 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'      ); });
  19. suite('search  data',  function()  {    var  xhr,  requests;    setup(function()

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

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

  24. /*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'); };
  25. /*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'  ] }