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.

94c767fbe5e2ce7ba74e9833c8e03d45?s=128

Damian Nicholson

May 09, 2013
Tweet

Transcript

  1. Writing testable, scalable, maintainable, rock-solid JavaScript

  2. How this can be achieved?

  3. Hello I am @damian

  4. None
  5. uk.sageone.com

  6. Scale & high growth

  7. Sage One Platform

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

    One USA Sage One Spain Sage One Germany
  9. JavaScript + ? = Good code

  10. JavaScript + Unit testing = Good code Tip 1

  11. Good code is a natural byproduct

  12. Twitter

  13. $('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(); } }) });
  14. How would I test this?

  15. I CAN’T!

  16. $('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
  17. “jQuery way”

  18. 1.AJAX 2.Event handling 3.DOM manipulation

  19. “If its hard to test its a good sign that

    you need to refactor your code”
  20. 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
  21. So whats the right way?

  22. Forces you to test through objects

  23. Leverage Objects to the max Tip 2

  24. None
  25. Single Responsibility Principle

  26. Decoupled and intuitive

  27. 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
  28. 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
  29. 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
  30. How much time should I spend testing?

  31. “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”
  32. What should I test?

  33. Your public API Tip 3

  34. How do I test my jQuery?

  35. By testing jQuery’s interface Tip 4

  36. 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); }); }); });
  37. 1.Unit tests are a win 2.JS objects are a win

    3.Only test your public API
  38. What next?

  39. Data-attributes

  40. 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)); } };
  41. Lifecycle hooks

  42. ajaxSuccess: function(data) { $({}).trigger(‘save.success’, data); this.reset(); }

  43. In action

  44. Wrap interface to plugins

  45. function Datepicker($el) { this.$el = $el; }; Datepicker.prototype = {

    hide: function() { this.$el.datepicker('hide'); }, show: function() { this.$el.datepicker('show'); } };
  46. Leverage mixins

  47. 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); });
  48. { mustache

  49. Members only

  50. (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);
  51. Thanks

  52. @damian damiannicholson.com

  53. Questions?