- Douglas Crockford So let’s take this nitroglycerin and throw it into the Fire Swamp. We’ll never survive. Nonsense, you’re only saying that because no one ever has.
woes Problems Google ended up writing thousands for GMail. Turns out, it’s unsustainable. Took hours for test suites to run Can’t rely on them as a safety net
baz'; expect(message).toMatch(/bar/); expect(message).toMatch('bar'); expect(message).not.toMatch(/quux/); }); Has tons of assertions built in, including regular expressions
var excited = makeExcited('nebraska code camp'); expect(excited).toBe('NEBRASKA CODE CAMP!!1'); }; it('handles no value gracefully', function() { expect(makeExcited()).toBeUndefined(); }; }); Here’s what its test would look like. You can test “happy path” But the key is testing edge cases. What happens if you pass an object?
dataType: 'json', data: {text: $(this).find('textarea').val()}, }); return false; }); }); We also want to pass data, so we’ll do a query to find the text inside the textarea
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses') .append('<li>' + data.text + '</li>'); } }); return false; }); }); And when the ajax call returns, we expect it’ll contain a “text” JSON field. We then find our #statuses field and create a new list item containing what got returned
Problem is, this is bad code. Any other programming language, we’d stop and refactor. Who was coding in something other than JS 5 years ago? Lots of us came to JS from other languages. We test there, right?
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Network I/O Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Network I/O User Input Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Network I/O Network Event User Input Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Page Load Form Event Network I/O Network Event User Input Templating Violates is so many ways Bound to page loading, so you only have one chance to test it Tied to form submission, so you have to do that Requires network activities, a textarea to have been filled in, an event triggered by successful ajax, and finally HTML templating.
dataType: 'json', data: { text: options.text }, success: options.success }); }; jQuery(function($) { $('form').on('submit', function() { addStatus({ text: $('textarea').val(), success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }); Still having to do lots of DOM queries inside addStatus. Let’s have it be handed the text it should be sending, as well as the jQuery callback Now addStatus is solely focused on handing ajax, nothing else.
url: '/statuses', type: 'POST', dataType: 'json', data: { text: options.text }, success: options.success }); }; Let’s keep going. We can create a Model object Using design pattern called Constructor with Prototype Functions are objects; can now manage it. Easier to reuse model if we want to. Encapsulate data
function() { statuses.add({ text: $('textarea').val(), success: function(data) { $('#statuses').append('<li>' + data.text + '</li>'); } }); return false; }); }; jQuery(function($) { var statuses = new Statuses(); new NewStatusView({statuses: statuses}); }); Still have lots inside page load. Let’s fix that Create a View object called NewStatusView Pass it our model object “statuses”. Now page load just assembles the elements together
this)); }; NewStatusView.prototype.addStatus = function(e) { e.preventDefault(); var that = this; this.statuses.add({ text: $('textarea').val(), success: function(data) { that.appendStatus(data.text); } }); return false; }; NewStatusView.prototype.appendStatus = function(text) { $('#statuses').append('<li>' + text + '</li>'); }; We can structure the view more. Extract the HTML templating to appendStatus Move the addStatus code to its own function JS changes “this” inside member functions. Rules are confusing, we use $.proxy here
a view with a model', function() { var statuses = new Statuses(), submitted = false; this.server.respondWith('{"text" : "Testing"}'); statuses.add({ text: 'Testing', success: function() { submitted = true; } }); this.server.respond(); expect(submitted).toBe(true); }); Here’s what a test looks like. Test our model. Fake out a server using Sinon Important code is where we new up Statuses and call Add. Totally isolated from the view. Can test the ajax behavior directly.
var statuses = { add: function() {} }; var fixture = $('<div><form><textarea /></form></div>'); spyOn(statuses, 'add'); var newStatusView = new NewStatusView({ statuses: statuses, el: fixture }); fixture.find('form').trigger('submit'); expect(statuses.add).toHaveBeenCalled(); }); }); Testing our view is similar. We’re creating a fake version of our model Using Jasmine feature called spies to do this Can verify the view called Add on the spy Also: we’re not tied to document.ready. We test the view in isolation
going. We expanded from 16 lines to 40. I think it’s better Each line is single responsibility. Could break it down further. Frameworks can help with this.
}; Monologue.Model.Status = Backbone.Model.extend({}); Monologue.Collection.Statuses = Backbone.Collection.extend({ model: Monologue.Model.Status, url: '/statuses' }); Not a backbone class Lots of concepts apply. Example: Backbone has a Model with REST-style sync. Just specify collection and endpoint URL
function(e) { var $input = this.$el.find('textarea'); this.collection.create({text: $input.val()}); return false; } }); Views can say “on form submit, trigger submit code in view” You pass in the form, it’s responsible for creating a new collection based on data All totally encapsulated
add: function(model) { this.$el.append(this.template(model)); }, template: function(model) { return this.make('li', {'class':'status'}, model.get('text')); } }); Adding to collection triggers events Can add another view that’s bound to this collection. When items added, work gets done Templating is really straightforward too
$('#new-status'), collection: statuses }); new Monologue.View.StatusList({ el: $('#statuses'), collection: statuses }); }); Here’s how you set up your app. Again, just creating models and injecting views into our code
{ $el = $("<form><textarea>Whee!</textarea></form>"); collection = new (Backbone.Collection.extend({ url: '/mock' })); spyOn(collection, 'create'); view = new Monologue.View.PostStatus({ el: $el, collection: collection }); }); }); Here’s what testing the view looks like We create a view with a fake Backbone collection and textarea
form", function() { $el.trigger('submit'); expect(collection.create) .toHaveBeenCalledWith({text: "Whee!"}); }); }); And by submitting the form, we verify the collection got a new element All the same concepts, now you’re just reducing the amount of code you have to write Testing models - way easier Can rely on dozens of tests from API. Don’t have to test ajax; they’ve done it!
be scared if you do these things. Can worry about other things, like browser perf, IE7, working on mobile, semicolons... well at least don’t have to worry about testing.