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

Javascript as a programming language - Paris Web

Javascript as a programming language - Paris Web

How to setup up a stable javascript continuous integration environment and why you need it. Through a real life example, the talk explains all the benefits of having a development process that brings real control over javascript codebase. A deep analysis of developer and webapps needs and of the tools that fit those requirements.

Paris Web (Paris, 14 October 2011)
@cedmax
http://cedmax.com

Marco Cedaro

October 14, 2011
Tweet

More Decks by Marco Cedaro

Other Decks in Programming

Transcript

  1. They said I am a... Frontend Cowboy Nicola Vitto Jr.

    Javascript Pervert Roberto Felter Perfect Stranger basically anyone else Hello, who’s speaking? Marco Cedaro @cedmax
  2. Actually I am: a Frontend Developer at Spreaker.com a conference

    organizer with From The Front Hello, who’s speaking? Marco Cedaro @cedmax
  3. Actually I am: a Frontend Developer at Spreaker.com a conference

    organizer with From The Front and a javascript pervert Hello, who’s speaking? Marco Cedaro @cedmax
  4. I believe I can fly Be brave. Take risks. Nothing

    can substitute experience. Paulo Coelho
  5. we had no control javascript was the land of the

    brave we were fearless and unconscious once upon a time
  6. we had no control javascript was the land of the

    brave we were fearless and unconscious we were proud of being like that once upon a time
  7. we didn't have IDEs & tools they did actually it

    was our own fault but it wasn't fair
  8. we didn't have IDEs & tools they did actually it

    was our own fault we were not ready but it wasn't fair
  9. we didn't have IDEs & tools they did actually it

    was our own fault we were not ready and javascript was neither but it wasn't fair
  10. something we can achieve less bandwidth and server load loading

    resources and content when needed performance boosts that can lead to better conversion rates
  11. something we can achieve less bandwidth and server load loading

    resources and content when needed performance boosts that can lead to better conversion rates cross platform development: less maintenance costs
  12. less bandwidth and server load loading resources and content when

    needed performance boosts that can lead to better conversion rates cross platform development: less maintenance costs money
  13. what's missing? If I had nine of my fingers missing

    I wouldn't type any slower. Mitch Hedberg
  14. the small web agency The designer introduces a slider on

    some websites: ”it’s cool on apple store”. The developer gets a jQuery plugin online
  15. the small web agency The designer introduces a slider on

    some websites: ”it’s cool on apple store”. The developer gets a jQuery plugin online Major release of the most used browser. A small fix has been released, they have to change 5 files in 5 different projects.
  16. the small web agency The designer introduces a slider on

    some websites: ”it’s cool on apple store”. The developer gets a jQuery plugin online Major release of the most used browser. A small fix has been released, they have to change 5 files in 5 different projects. Oh damn! There’s no mouse wheel integration! should they ask for support or should they change the library by themself?
  17. the big corp The client-side architecture has been built on

    the most known and supported framework 2006
  18. the big corp The client-side architecture has been built on

    the most known and supported framework 2006 Everything seems to be fine, except that the well known framework was being replaced by a powerful new one 2008 - 2010
  19. the big corp The client-side architecture has been built on

    the most known and supported framework 2006 Everything seems to be fine, except that the well known framework was being replaced by a powerful new one 2008 - 2010 They were forced to change the whole codebase at once to reduce maintenance and development costs 2011
  20. continuous integration A software development practice where members of a

    team integrate their work frequently [...] to detect integration errors as quickly as possible. Martin Fowler
  21. choose your tools A man cannot be too careful in

    the choice of his enemies Oscar Wilde
  22. how does it work? write a test let it fail

    write the code run the test again
  23. how does it work? write a test let it fail

    write the code run the test again refactor
  24. The curious case of Javascript unit testing Unit testing is

    supposed to test a single atomic “unit” of functionality without dependencies on anything else
  25. The curious case of Javascript unit testing Unit testing is

    supposed to test a single atomic “unit” of functionality without dependencies on anything else This is where you start to run into serious dependency problems due to the interrelation with HTML and CSS
  26. The curious case of Javascript unit testing Unit testing is

    supposed to test a single atomic “unit” of functionality without dependencies on anything else This is where you start to run into serious dependency problems due to the interrelation with HTML and CSS What do you test? Usually how the user interface responds to user input. Actually, the realm of functional testing
  27. _$ = (function() { var registered = {}; return {

    ! ! pub: function(event, memo) { ! ! ! if (registered[event] instanceof Array){ ! ! ! ! var handlers = [].concat(registered[event]); ! ! ! ! for (var i=0, h; (h = handlers[i]); i++){ ! ! ! ! ! h.call(this, memo); ! ! ! ! } ! ! ! } ! ! }, ! ! sub: function(event, handler) { ! ! ! if (typeof registered[event] === "undefined"){ ! ! ! ! registered[event] = []; ! ! ! } ! ! ! registered[event].push(handler); ! ! } }; })();
  28. _$ = (function() { var registered = {}; return {

    ! ! pub: function(event, memo) { ! ! ! if (registered[event] instanceof Array){ ! ! ! ! var handlers = [].concat(registered[event]); ! ! ! ! for (var i=0, h; (h = handlers[i]); i++){ ! ! ! ! ! h.call(this, memo); ! ! ! ! } ! ! ! } ! ! }, ! ! sub: function(event, handler) { ! ! ! if (typeof registered[event] === "undefined"){ ! ! ! ! registered[event] = []; ! ! ! } ! ! ! registered[event].push(handler); ! ! } }; })();
  29. _$ = (function() { var registered = {}; return {

    ! ! pub: function(event, memo) { ! ! ! if (registered[event] instanceof Array){ ! ! ! ! var handlers = [].concat(registered[event]); ! ! ! ! for (var i=0, h; (h = handlers[i]); i++){ ! ! ! ! ! h.call(this, memo); ! ! ! ! } ! ! ! } ! ! }, ! ! sub: function(event, handler) { ! ! ! if (typeof registered[event] === "undefined"){ ! ! ! ! registered[event] = []; ! ! ! } ! ! ! registered[event].push(handler); ! ! } }; })();
  30. _$ = (function (_) { ! return { ! !

    pub: function(a, b, c, d) { ! ! ! for (d=-1, c=[].concat(_[a]); c[++d];) c[d](b) ! ! }, ! ! sub: function(a, b) { ! ! ! (_[a] || (_[a] = [])).push(b) ! ! } ! } })({})
  31. _$ = (function (_) { ! return { ! !

    pub: function(a, b, c, d) { ! ! ! for (d=-1, c=[].concat(_[a]); c[++d];) c[d](b) ! ! }, ! ! sub: function(a, b) { ! ! ! (_[a] || (_[a] = [])).push(b) ! ! } ! } })({}) #140bytes
  32. _$ = (function() { var registered = {}; return {

    ! ! pub: function(event, memo) { ! ! ! if (registered[event] instanceof Array){ ! ! ! ! var handlers = [].concat(registered[event]); ! ! ! ! for (var i=0, h; (h = handlers[i]); i++){ ! ! ! ! ! h.call(this, memo); ! ! ! ! } ! ! ! } ! ! }, ! ! sub: function(event, handler) { ! ! ! if (typeof registered[event] === "undefined"){ ! ! ! ! registered[event] = []; ! ! ! } ! ! ! registered[event].push(handler); ! ! } }; })();
  33. _$ = (function (_) { ! return { ! !

    pub: function(a, b, c, d) { ! ! ! for (d=-1, c=[].concat(_[a]); c[++d];) c[d](b) ! ! }, ! ! sub: function(a, b) { ! ! ! (_[a] || (_[a] = [])).push(b) ! ! } ! } })({}) #140bytes
  34. _$ = (function() { var registered = {}; return {

    ! ! pub: function(event, memo) { ! ! ! if (registered[event] instanceof Array){ ! ! ! ! var handlers = [].concat(registered[event]); ! ! ! ! for (var i=0, h; (h = handlers[i]); i++){ ! ! ! ! ! h.call(this, memo); ! ! ! ! } ! ! ! } ! ! }, ! ! sub: function(event, handler) { ! ! ! if (typeof registered[event] === "undefined"){ ! ! ! ! registered[event] = []; ! ! ! } ! ! ! registered[event].push(handler); ! ! } }; })();
  35. _$ = (function (_) { ! return { ! !

    pub: function(a, b, c, d) { ! ! ! for (d=-1, c=[].concat(_[a]); c[++d];) c[d](b) ! ! }, ! ! sub: function(a, b) { ! ! ! (_[a] || (_[a] = [])).push(b) ! ! } ! } })({}) #140bytes
  36. _$ = (function (_) { ! return { ! !

    pub: function(a, b, c, d) { ! ! ! for (d=-1, c=[].concat(_[a]); c[++d];) c[d](b) ! ! }, ! ! sub: function(a, b) { ! ! ! (_[a] || (_[a] = [])).push(b) ! ! } ! } })({}) #140bytes
  37. [...] testPub: function(){! ! ! ! var a = 0;!

    ! ! _$.sub('testNotify', function(){ a = 1; }); ! ! _$.pub('testNotify'); ! ! assertEquals(1, a); ! }, ! ! ! ! testNotifyWithMemo: function(){!! ! ! var a = 0 ;! ! ! _$.sub('testNotify', function(memo){ ! ! ! ! a = memo.test; ! ! }); ! ! _$.pub('testNotify', {test: 1}); ! ! assertEquals(1, a); ! }, [...]
  38. [...] testPub: function(){! ! ! ! var a = 0;!

    ! ! _$.sub('testNotify', function(){ a = 1; }); ! ! _$.pub('testNotify'); ! ! assertEquals(1, a); ! }, ! ! ! ! testNotifyWithMemo: function(){!! ! ! var a = 0 ;! ! ! _$.sub('testNotify', function(memo){ ! ! ! ! a = memo.test; ! ! }); ! ! _$.pub('testNotify', {test: 1}); ! ! assertEquals(1, a); ! }, [...]
  39. [...] testPub: function(){! ! ! ! var a = 0;!

    ! ! _$.sub('testNotify', function(){ a = 1; }); ! ! _$.pub('testNotify'); ! ! assertEquals(1, a); ! }, ! ! ! ! testNotifyWithMemo: function(){!! ! ! var a = 0 ;! ! ! _$.sub('testNotify', function(memo){ ! ! ! ! a = memo.test; ! ! }); ! ! _$.pub('testNotify', {test: 1}); ! ! assertEquals(1, a); ! }, [...]
  40. [...] testPub: function(){! ! ! ! var a = 0;!

    ! ! _$.sub('testNotify', function(){ a = 1; }); ! ! _$.pub('testNotify'); ! ! assertEquals(1, a); ! }, ! ! ! ! testNotifyWithMemo: function(){!! ! ! var a = 0 ;! ! ! _$.sub('testNotify', function(memo){ ! ! ! ! a = memo.test; ! ! }); ! ! _$.pub('testNotify', {test: 1}); ! ! assertEquals(1, a); ! }, [...]
  41. [...] testPub: function(){! ! ! ! var a = 0;!

    ! ! _$.sub('testNotify', function(){ a = 1; }); ! ! _$.pub('testNotify'); ! ! assertEquals(1, a); ! }, ! ! ! ! testNotifyWithMemo: function(){!! ! ! var a = 0 ;! ! ! _$.sub('testNotify', function(memo){ ! ! ! ! a = memo.test; ! ! }); ! ! _$.pub('testNotify', {test: 1}); ! ! assertEquals(1, a); ! }, [...]
  42. [...] testPub: function(){! ! ! ! var a = 0;!

    ! ! _$.sub('testNotify', function(){ a = 1; }); ! ! _$.pub('testNotify'); ! ! assertEquals(1, a); ! }, ! ! ! ! testNotifyWithMemo: function(){!! ! ! var a = 0 ; ! ! _$.sub('testNotify', function(memo){ ! ! ! ! a = memo.test; ! ! }); ! ! _$.pub('testNotify', {test: 1}); ! ! assertEquals(1, a); ! }, [...]
  43. [...] testPub: function(){! ! ! ! var a = 0;!

    ! ! _$.sub('testNotify', function(){ a = 1; }); ! ! _$.pub('testNotify'); ! ! assertEquals(1, a); ! }, ! ! ! ! testNotifyWithMemo: function(){!! ! ! var a = 0 ;! ! ! _$.sub('testNotify', function(memo){ ! ! ! ! a = memo.test; ! ! }); ! ! _$.pub('testNotify', {test: 1}); ! ! assertEquals(1, a); ! }, [...]
  44. a mock library SPIES a function that records arguments, return

    value, the value of this and exception thrown (if any) for all its calls.
  45. a mock library SPIES a function that records arguments, return

    value, the value of this and exception thrown (if any) for all its calls. STUBS functions (spies) with pre-programmed behavior.
  46. a mock library SPIES a function that records arguments, return

    value, the value of this and exception thrown (if any) for all its calls. STUBS functions (spies) with pre-programmed behavior. MOCKS functions (spies) with pre-programmed behavior (stubs) as well as pre-programmed expectations.
  47. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SysyemOn')); ! }, ! ! ! ! testMyLibLoggedNotLogged: function(){!! ! ! var stub = sinon.stub(User, 'isLogged');! ! ! stub.returns(true); ! ! //DO STUFF && ASSERTIONS ! ! stub.returns(false); ! ! //DO STUFF && ASSERTIONS ! }, [...]
  48. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SystemOn')); ! }, ! ! ! ! testMyLibLoggedNotLogged: function(){!! ! ! var stub = sinon.stub(User, 'isLogged');! ! ! stub.returns(true); ! ! //DO STUFF && ASSERTIONS ! ! stub.returns(false); ! ! //DO STUFF && ASSERTIONS ! }, [...] SPY
  49. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SystemOn')); ! }, ! ! ! ! testMyLibLoggedNotLogged: function(){!! ! ! var stub = sinon.stub(User, 'isLogged');! ! ! stub.returns(true); ! ! //DO STUFF && ASSERTIONS ! ! stub.returns(false); ! ! //DO STUFF && ASSERTIONS ! }, [...] SPY
  50. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SystemOn')); ! }, ! ! ! ! testMyLibLoggedNotLogged: function(){!! ! ! var stub = sinon.stub(User, 'isLogged');! ! ! stub.returns(true); ! ! //DO STUFF && ASSERTIONS ! ! stub.returns(false); ! ! //DO STUFF && ASSERTIONS ! }, [...] SPY
  51. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SystemOn')); ! }, ! ! ! ! testMyLibLoggedNotLogged: function(){!! ! ! var stub = sinon.stub(User, 'isLogged');! ! ! stub.returns(true); ! ! //DO STUFF && ASSERTIONS ! ! stub.returns(false); ! ! //DO STUFF && ASSERTIONS ! }, [...] SPY
  52. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SystemOn')); ! }, ! ! ! ! testMyLibLoggedNotLogged: function(){!! ! ! var stub = sinon.stub(User, 'isLogged');! ! ! stub.returns(true); ! ! //DO STUFF && ASSERTIONS ! ! stub.returns(false); ! ! //DO STUFF && ASSERTIONS ! }, [...] STUB
  53. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SystemOn')); ! }, ! ! ! ! testMyLibLoggedNotLogged: function(){!! ! ! var stub = sinon.stub(User, 'isLogged');! ! ! stub.returns(true); ! ! //DO STUFF && ASSERTIONS ! ! stub.returns(false); ! ! //DO STUFF && ASSERTIONS ! }, [...] STUB
  54. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SystemOn')); ! }, ! ! ! ! testMyLibLoggedNotLogged: function(){!! ! ! var stub = sinon.stub(User, 'isLogged');! ! ! stub.returns(true); ! ! //DO STUFF && ASSERTIONS ! ! stub.returns(false); ! ! //DO STUFF && ASSERTIONS ! }, [...] STUB
  55. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SystemOn')); ! }, ! ! ! ! testMyLibLoggedNotLogged: function(){!! ! ! var stub = sinon.stub(User, 'isLogged');! ! ! stub.returns(true); ! ! //DO STUFF && ASSERTIONS ! ! stub.returns(false); ! ! //DO STUFF && ASSERTIONS ! }, [...] STUB
  56. [...] testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! assertTrue(spy.calledWith('SystemOn')); ! }, [...] MOCK
  57. [...] //testMyLibRegistersToSystemOnEvent: function(){! ! ! ! //var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! //assertTrue(spy.calledWith('SysyemOn')); ! //}, ! ! testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var mock = sinon.mock(_$); ! ! mock.expect('watch').calledWith('SysyemOn'); ! ! //DO STUFF TO INIT YOUR LIB ! ! mock.verify(); ! }, [...] MOCK
  58. [...] //testMyLibRegistersToSystemOnEvent: function(){! ! ! ! //var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! //assertTrue(spy.calledWith('SysyemOn')); ! //}, ! ! testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var mock = sinon.mock(_$); ! ! mock.expect('watch').calledWith('SysyemOn'); ! ! //DO STUFF TO INIT YOUR LIB ! ! mock.verify(); ! }, [...] MOCK
  59. [...] //testMyLibRegistersToSystemOnEvent: function(){! ! ! ! //var spy = sinon.spy(_$,

    'watch');! ! ! //DO STUFF TO INIT YOUR LIB ! ! //assertTrue(spy.calledWith('SysyemOn')); ! //}, ! ! testMyLibRegistersToSystemOnEvent: function(){! ! ! ! var mock = sinon.mock(_$); ! ! mock.expect('watch').calledWith('SysyemOn'); ! ! //DO STUFF TO INIT YOUR LIB ! ! mock.verify(); ! }, [...] MOCK
  60. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  61. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  62. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  63. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  64. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  65. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  66. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  67. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  68. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  69. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  70. [...] testOnSuccessCallback: function(){! ! var server = sinon.useFakeServer(); server.respondWith("GET", "/art/12/comments.json",

    [200, {"Content-Type":"application/json"}, "[{ id:12, text:'Hello'}]"]); ! ! var spy = sinon.spy(); ! ! myLib.getCommentsFor("/art/12", spy); ! ! server.respond(); ! ! assert(spy.calledWith([{ id:12, text:"Hello"}])); ! }, [...] AJAX CALL
  71. IN THE WILD In the wild, there is no health

    care. Dwight Schrute (the office)
  72. the further we look at, the more control we need

    LOOKING FORWARD let's get ready
  73. the further we look at, the more control we need

    LOOKING FORWARD let's get ready javascript is a programming language
  74. the further we look at, the more control we need

    LOOKING FORWARD let's get ready javascript is a programming language javascript is a serious business.
  75. the further we look at, the more control we need

    LOOKING FORWARD let's get ready javascript is a programming language javascript is a serious business. and, most of all...