Share
Embed
The Plight of Pinocchio
JavaScript's quest to become a real language
JavaScript is no longer a toy language. Many of our applications can't function without it. If we are going to use JavaScript to do real things, we need to treat it like a real language, adopting the same practices we use with real languages.
This framework agnostic talk takes a serious look at how we develop JavaScript applications. Despite its prototypical nature, good object-oriented programming principles are still relevant. The design patterns that we've grown to know and love work just as well in JavaScript as they do any other language. Test driven development forces us to write modular, decoupled code.
Brandon Keepers PRO
May 16, 2012
Transcript
145 slides
-
If we are going to use JavaScript to do real
work, then we have to do the things that we do with real languages. -
in real languages, we... Use Test Driven Development Refactor Separate
Concerns Create Abstractions -
in real languages, we... Use Test Driven Development Apply Design
Patterns Refactor Separate Concerns Create Abstractions -
in real languages, we... Use Test Driven Development Apply Design
Patterns Decouple Refactor Separate Concerns Create Abstractions -
in real languages, we... Use Test Driven Development Apply Design
Patterns Decouple Refactor Separate Concerns Use Encapsulation Create Abstractions -
in real languages, we... Use Test Driven Development Apply Design
Patterns Decouple Refactor Separate Concerns Use Encapsulation Create Abstractions DRY -
why? Use Test Driven Development Apply Design Patterns Decouple Refactor
Separate Concerns Use Encapsulation Create Abstractions DRY -
Litmus Test If your site or application breaks without JavaScript,
then you should treat it like a real language. -
a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { return
false; }); }); example lovingly stolen from @searls -
a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({
url: '/statuses', type: 'POST', }); return false; }); }); example lovingly stolen from @searls -
a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({
url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, }); return false; }); }); example lovingly stolen from @searls -
a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({
url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { } }); return false; }); }); example lovingly stolen from @searls -
a typical JavaScript example... jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({
url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append( '<li>' + data.text + '</li>'); } }); return false; }); }); example lovingly stolen from @searls -
Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce
“Starting with a test means that we have to describe what we want to achieve before we consider how.” -
“ Testing JavaScript is not worth it.” “It changes too
frequently.” “The tests break all the time.” -
“ Testing JavaScript is not worth it.” “It changes too
frequently.” “It’s just view code.” “The tests break all the time.” -
“ Testing JavaScript is not worth it.” “It changes too
frequently.” “Browser implementations are too different.” “It’s just view code.” “The tests break all the time.” -
“ Testing JavaScript is not worth it.” “It changes too
frequently.” “Browser implementations are too different.” “It’s just view code.” “The tests break all the time.” “It’s too hard.” -
jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... example lovingly stolen from @searls -
jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event example lovingly stolen from @searls -
jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event example lovingly stolen from @searls -
jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event 3. network IO example lovingly stolen from @searls -
jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event 3. network IO 4. user input example lovingly stolen from @searls -
jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event 3. network IO 4. user input 5. network event example lovingly stolen from @searls -
jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... 1. page event 2. user event 3. network IO 4. user input 5. network event 6. HTML templating example lovingly stolen from @searls -
“The same structure that makes the code difficult to test
now will make it difficult to change in the future.” Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce -
system tests with cucumber... Feature: Telling the world about my
every breath Scenario: Successfully posting a status update When I am on the home page And I fill in "Status" with "Integration tests are easy" And I press "Post Update" Then I should see "Integration tests are easy" -
system tests with cucumber... $ cucumber Feature: Posting Statuses Scenario:
Successfully posting a status When I am on the home page And I fill in "Status" with "Integration tests are easy" And I press "Post Update" Then I should see "Integration tests are easy" 1 scenario (1 passed) 4 steps (4 passed) 0m0.988s -
there are many advantages to system testing easy to write
relatively easy to maintain -
there are many advantages to system testing easy to write
run in real browser environment relatively easy to maintain -
there are many advantages to system testing easy to write
run in real browser environment verifies system actually works relatively easy to maintain -
there are many disadvantages to system testing test failures are
difficult to decipher run very slow -
there are many disadvantages to system testing harder to simulate
failure cases test failures are difficult to decipher run very slow -
there are many disadvantages to system testing harder to simulate
failure cases test failures are difficult to decipher run very slow does not encourage good design -
System tests tell you your application is functioning, but tell
you nothing about the internal state. -
Me Just Now™ “With system tests, write as few as
possible, but no fewer.” -
“To construct an object for a unit test, we have
to pass its dependencies to it, which means that we have to know what they are.” Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce -
“To keep unit tests understandable (and, so, maintainable), we have
to limit their scope.” Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce -
a unit test in jasmine require('/js/views/status_list.js'); describe("View.StatusList", function() { beforeEach(function()
{ this.view = new View.StatusList(); }); it("fetches records from the server", function() { expect(this.view.collection.fetch).toHaveBeenCalled(); }); it("renders when collection is reset", function() { this.view.collection.reset([{text: 'Unit testing is fun'}]); expect(this.view.$el.find('li').text()).toEqual('Unit testing is fun'); }); it("appends newly added items", function() { -
Architect Christopher Alexander “Each pattern describes a problem that occurs
over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice.” -
Design patterns are the basic parts of speech that allow
us to create coherent, well-structured systems. -
Creational Abstract Factory Builder Factory Method Prototype Singleton Structural Adapter
Bridge Composite Decorator Flyweight Proxy Behavioral Chain of responsibility Command Interpreter Iterator Mediator Memento Observer State Strategy Template method Visitor “Classical Design Patterns” -
Advantages of Design Patterns removes duplication shared vocabulary generic, reusable
components faster communication -
Advantages of Design Patterns removes duplication shared vocabulary generic, reusable
components faster communication proven paradigms -
Advantages of Design Patterns removes duplication shared vocabulary generic, reusable
components faster communication proven paradigms easier to test -
Advantages of Design Patterns removes duplication shared vocabulary generic, reusable
components faster communication proven paradigms easier to test supports system changes -
Addy Osmani Learning JavaScript Design Patterns “Patterns don’t solve all
design problems, nor do they replace good software designers, however, they do support them.” -
Martin Fowler http://www.martinfowler.com/ “The idea behind [MVC] is to make
a clear division between domain objects that model our perception of the real world, and presentation objects that are the GUI elements we see on the screen.” -
Martin Fowler http://www.martinfowler.com/ “...Domain objects should be completely self contained
and work without reference to the presentation, they should also be able to support multiple presentations, possibly simultaneously.” -
jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append( '<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... -
jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST',
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append( '<li>' + data.text + '</li>'); } }); return false; }); }); back to our example... -
refactoring to use models... // models/status.js Model.Status = Backbone.Model.extend({ });
// collections/statuses.js Collection.Statuses = Backbone.Collection.extend({ model: Model.Status, url: '/statuses' }); -
and the result... // app.js jQuery(function($) { var statuses =
new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({ text: $(this).find('textarea').val() }); return false; }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); -
and the result... // app.js jQuery(function($) { var statuses =
new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({ text: $(this).find('textarea').val() }); return false; }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); Observer Pattern -
Our models don’t have logic yet, so we’re skipping the
unit test for now. -
but our system test still passes... $ cucumber Feature: Posting
Statuses Scenario: Succesfully posting a status When I am on the home page And I fill in "Status" with "Integration tests are easy" And I press "Post Update" Then I should see "Integration tests are easy" 1 scenario (1 passed) 4 steps (4 passed) 0m0.988s -
back to our example... // app.js jQuery(function($) { var statuses
= new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({text: $(this).find('textarea').val()}); return false; }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); -
back to our example... // app.js jQuery(function($) { var statuses
= new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({text: $(this).find('textarea').val()}); return false; }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); -
test driven refactoring... describe("PostStatus", function() { describe("submitting the form", function()
{ it("creates a status", function() { }); }); }); -
test driven refactoring... describe("PostStatus", function() { describe("submitting the form", function()
{ it("creates a status", function() { $el.trigger('submit'); }); }); }); -
test driven refactoring... describe("PostStatus", function() { describe("submitting the form", function()
{ it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); }); -
test driven refactoring... describe("PostStatus", function() { var $el; beforeEach(function() {
$el = $("<form><textarea>It's easy!</textarea></form>"); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); }); -
test driven refactoring... describe("PostStatus", function() { var $el; beforeEach(function() {
$el = $("<form><textarea>It's easy!</textarea></form>"); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); }); unattached from DOM -
test driven refactoring... describe("PostStatus", function() { var $el, collection; beforeEach(function()
{ $el = $("<form><textarea>It's easy!</textarea></form>"); collection = new Collection.Statuses(); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); }); -
test driven refactoring... describe("PostStatus", function() { var $el, collection; beforeEach(function()
{ $el = $("<form><textarea>It's easy!</textarea></form>"); collection = new Collection.Statuses(); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); }); do we need this test dependency? -
test driven refactoring... describe("PostStatus", function() { var $el, collection; beforeEach(function()
{ $el = $("<form><textarea>It's easy!</textarea></form>"); collection = {create: jasmine.createSpy('create')}; }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( {text: "It’s easy!"}); }); }); }); -
test driven refactoring... describe("PostStatus", function() { var $el, collection, view;
beforeEach(function() { $el = $("<form><textarea>It's easy!</textarea></form>"); collection = {create: jasmine.createSpy('create')}; view = new View.PostStatus({ el: $el, collection: collection }); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( -
test driven refactoring... describe("PostStatus", function() { var $el, collection, view;
beforeEach(function() { $el = $("<form><textarea>It's easy!</textarea></form>"); collection = {create: jasmine.createSpy('create')}; view = new View.PostStatus({ el: $el, collection: collection }); }); describe("submitting the form", function() { it("creates a status", function() { $el.trigger('submit'); expect(collection.create).toHaveBeenCalledWith( dependency injection -
refactoring to use controllers... // views/post_status.js View.PostStatus = Backbone.View.extend({ events:
{ "submit": "submit" }, submit: function(e) { var $input = this.$el.find('textarea'); this.collection.create({text: $input.val()}); return false; } }); -
and the result... // app.js jQuery(function($) { var statuses =
new Collection.Statuses(); new View.PostStatus({ el: $('#new-status'), collection: statuses }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); -
and our system test still passes... $ cucumber Feature: Posting
Statuses Scenario: Succesfully posting a status When I am on the home page And I fill in "Status" with "Integration tests are easy" And I press "Post Update" Then I should see "Integration tests are easy" 1 scenario (1 passed) 4 steps (4 passed) 0m0.988s -
// app.js jQuery(function($) { var statuses = new Collection.Statuses(); new
View.PostStatus({ el: $('#new-status'), collection: statuses }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); back to our example... -
// app.js jQuery(function($) { var statuses = new Collection.Statuses(); new
View.PostStatus({ el: $('#new-status'), collection: statuses }); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); back to our example... -
test driven refactoring... describe("View.StatusList", function() { it("appends newly added items",
function() { collection.add({text: 'this is fun!'}); expect($el.find('li').length).toBe(1); expect($el.find('li').text()).toEqual('this is fun!'); }); }); -
test driven refactoring... describe("View.StatusList", function() { beforeEach(function() { $el =
$('<ul></ul>'); collection = new Backbone.Collection(); view = new View.StatusList({ el: $el, collection: collection }); }); it("appends newly added items", function() { collection.add({text: 'this is fun!'}); expect($el.find('li').length).toBe(1); expect($el.find('li').text()).toEqual('this is fun!'); }); }); -
refactoring to use views... // views/status_list.js View.StatusList = Backbone.View.extend({ initialize:
function() { _.bindAll(this, 'add'); this.collection.on('add', this.add); }, add: function(model) { this.$el.append(this.template(model)) }, template: function(model) { return this.make('li', {className:'status'}, model.get('text')); } }); -
and the result... // app.js jQuery(function($) { var statuses =
new Collection.Statuses(); new View.PostStatus({ el: $('#new-status'), collection: statuses }); new View.StatusList({ el: $('#statuses'), collection: statuses }); }); -
adding new features, the cowboy way jQuery(function($) { $.ajax({ url:
'/statuses', dataType: 'json', success: function(data) { var $statuses = $('#statuses'); for(var i = 0; data.length > i; i++) { $('#statuses').append( '<li>' + data[i].text + '</li>'); } } }); }); -
but we’re better than that now... Feature: Telling the world
about my every breath # ... Scenario: Viewing previous status updates Given the following statuses exist: | This is outside-in development | | It’s not so bad | When I am on the home page Then I should see "This is outside-in development" And I should see "It’s not so bad" -
failing system test... $ cucumber features/statuses.feature:9 Feature: Posting Statuses Scenario:
Viewing previous status updates Given the following statuses exist: | This is outside-in development | | It's not so bad | When I am on the home page Then I should see "This is outside-in development" expected there to be content "This is outside-in development" in "Post Update" (RSpec::Expectations::ExpectationNotMetError) And I should see "It's not so bad" Failing Scenarios: cucumber features/statuses.feature:9 1 scenario (1 failed) 4 steps (1 failed, 1 skipped, 3 passed) 0m2.619s -
then a unit test... describe("View.StatusList", function() { // … it("fetches
records from the server", function() { expect(collection.fetch).toHaveBeenCalled(); }); it("renders when collection is reset", function() { collection.reset([{text: 'This is fun!'}]); expect($el.find('li').length).toBe(1); expect($el.find('li').text()).toEqual('This is fun!'); }); }); -
and finally, the implementation... // views/status_list.js View.StatusList = Backbone.View.extend({ initialize:
function() { this.collection.on('add', this.add); }, add: function(model) { this.$el.append(this.template(model)) }, template: function(model) { return this.make('li', {className:'status'}, model.get('text')); } }); -
and finally, the implementation... // views/status_list.js View.StatusList = Backbone.View.extend({ initialize:
function() { this.collection.on('add', this.add); this.collection.on('reset', this.render); this.collection.fetch(); }, render: function() { this.collection.each(this.add); }, add: function(model) { this.$el.append(this.template(model)) }, // ... template: function(model) { -
our top level code didn’t change... // app.js jQuery(function($) {
var statuses = new Collection.Statuses(); new View.PostStatus({ el: $('#new-status'), collection: statuses }); new View.StatusList({ el: $('#statuses'), collection: statuses }); }); -
MVC is just one design pattern. There are many others
that can help us. -
Robert C. Martin Clean Code “Writing clean code requires the
disciplined use of a myriad of little techniques applied through a painstakingly acquired sense of ‘cleanliness.’ ” -
single responsibility principle Every object should have a single responsibility,
and that responsibility should be entirely encapsulated by the object. -
“Our heuristic is that we should be able to describe
what an object does without using any conjunctions (‘and’, ‘or’).” Growing Object-Oriented Software Guided by Tests Steve Freeman, Nat Pryce -
responsibility & dependencies // Responsibility: // Create a status from
user input // // Dependencies: // * el - A form element with a textarea // * collection - the collection that the status will be // created in View.PostStatus = Backbone.View.extend({ // ... }); -
Carlo Collodi Adventures of Pinocchio “One day I was about
to become a boy, a real boy, but on account of my laziness and my hatred of books, and because I listened to bad companions…I awoke to find myself changed into a donkey– long ears, gray coat, even a tail!”