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

Writing testable, scalable, maintainable rock-solid JavaScript

Writing testable, scalable, maintainable rock-solid JavaScript

This talk was given at Scotland.js, in Edinburgh, 2013.

A talk focussed on crafting JavaScript code for longevity, and how to best achieve that using unit testing and encapsulating your business logic as objects, rather than writing code in the "jQuery way".

Also presented are some techniques to ensure that JavaScript code is adaptable and loosely coupled.

Damian Nicholson

May 09, 2013
Tweet

More Decks by Damian Nicholson

Other Decks in Programming

Transcript

  1. Sage One Platform Ruby on Rails Sage One UK Sage

    One USA Sage One Spain Sage One Germany
  2. $('form#tweet').on('submit', function(e) { e.preventDefault(); var _this = this, $tweets =

    $('#tweets'), $this = $(this), $textarea = $this.find('textarea'), tweet = $textarea.val(); $.ajax({ url: $this.attr('action'), type: $this.attr('method'), success: function() { $tweets.append('<li>' + tweet + '</li>'); _this.reset(); } }) });
  3. $('form#tweet').on('submit', function(e) { e.preventDefault(); var _this = this, $tweets =

    $('#tweets'), $this = $(this), $textarea = $this.find('textarea'), tweet = $textarea.val(); $.ajax({ url: $this.attr('action'), type: $this.attr('method'), success: function() { $tweets.append('<li>' + tweet + '</li>'); _this.reset(); } }) }); Anonymous function HTML templating Hitting the server Tight coupling Nested callback
  4. “If its hard to test its a good sign that

    you need to refactor your code”
  5. Anonymous function HTML templating Hitting the server Tight coupling $('form#tweet').on('submit',

    function(e) { e.preventDefault(); var _this = this, $tweets = $('#tweets'), $this = $(this), $textarea = $this.find('textarea'), tweet = $textarea.val(); $.ajax({ url: $this.attr('action'), type: $this.attr('method'), success: function() { $tweets.append('<li>' + tweet + '</li>'); _this.reset(); } }) }); Nested callback
  6. function AjaxForm($el) { this.$form = $el; this.form = $el[0]; this.$form.on('submit',

    $.proxy(this.submit, this)); }; AjaxForm.prototype = { submit: function(e) { e.preventDefault(); var _this = this; $.ajax({ url: _this.form.action, type: _this.form.method, success: $.proxy(this.onSuccess, _this) }); }, onSuccess: function(data) { $({}).trigger('save.success', data); this.reset(); }, reset: function() { this.form.reset(); } }; Loose coupling Single responsibility principle Named functions Internal state via properties
  7. describe("An AJAX form", function() { var inst, $el; beforeEach(function() {

    $el = $('<form action="/foo" method="post" />'); spyOn($.fn, 'on').andCallThrough(); inst = new AjaxForm($el); }); describe("initialize", function() { it("should have a reference to the jQuery form object", function() { expect(inst.$form instanceof jQuery).toBeTruthy(); }); it("should have a reference to the form object", function() { expect(inst.form).toBe($el[0]); }); it("should set up a submit event handler on the form object", function() { expect($.fn.on.mostRecentCall.args[0]).toEqual('submit'); }); }); Assert against properties Assert on things being called
  8. describe("when the form is submitted", function() { var submitEv; beforeEach(function()

    { spyOn($, 'ajax'); submitEv = $.Event('submit'); inst.$form.trigger(submitEv); }); it("should prevent the default form submit action", function() { expect(submitEv.isDefaultPrevented()).toBeTruthy(); }); describe("the AJAX request", function() { it("should make a POST", function() { expect($.ajax.mostRecentCall.args[0].type).toEqual('post'); }); it("should go to /foo", function() { expect($.ajax.mostRecentCall.args[0].url.indexOf('/foo')).not.toBe(-1); }); }); }); Assert events Assert AJAX options
  9. “I get paid for code that works, not for tests,

    so my philosophy is to test as little as possible to reach a given level of confidence”
  10. describe("when the form is submitted", function() { var submitEv; beforeEach(function()

    { spyOn($, 'ajax'); submitEv = $.Event('submit'); inst.$form.trigger(submitEv); }); it("should prevent the default form submit action", function() { expect(submitEv.isDefaultPrevented()).toBeTruthy(); }); describe("the AJAX request", function() { it("should make a POST", function() { expect($.ajax.mostRecentCall.args[0].type).toEqual('post'); }); it("should go to /foo", function() { expect($.ajax.mostRecentCall.args[0].url.indexOf('/foo')).not.toBe(-1); }); }); });
  11. 1.Unit tests are a win 2.JS objects are a win

    3.Only test your public API
  12. var Form = function($el) { this.$el = $el; this.el =

    $el[0]; var isAJAX = this.$el.data('ajax'); if (isAJAX) { this.$el.on('submit', $.proxy(this.ajaxSubmit, this)); } };
  13. function Datepicker($el) { this.$el = $el; }; Datepicker.prototype = {

    hide: function() { this.$el.datepicker('hide'); }, show: function() { this.$el.datepicker('show'); } };
  14. var FormInputMixin = { getValue: function() { return this.$el.val(); },

    setValue: function(val) { return this.$el.val(val); } }; var FormInputs = ['Textbox', 'Select', 'Checkbox']; $.each(FormInputs, function(input) { $.extend(input.prototype, FormInputMixin); });
  15. (function(exports, $) { function Form($el) { this.$el = $el; this.form

    = $el[0]; var isAjax = isAjaxEnabled.call(this); if (isAjax) { this.$el.on('submit', $.proxy(this.ajaxSubmit, this)); } }; ... function isAjaxEnabled() { return this.$el.data('ajax'); } exports.Form = Form; })(window, jQuery);