$30 off During Our Annual Pro Sale. View Details »

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. Writing testable, scalable,
    maintainable, rock-solid
    JavaScript

    View Slide

  2. How this can be achieved?

    View Slide

  3. Hello I am @damian

    View Slide

  4. View Slide

  5. uk.sageone.com

    View Slide

  6. Scale & high
    growth

    View Slide

  7. Sage One Platform

    View Slide

  8. Sage One Platform
    Ruby on Rails
    Sage One UK
    Sage One USA
    Sage One Spain
    Sage One Germany

    View Slide

  9. JavaScript + ? = Good code

    View Slide

  10. JavaScript + Unit testing = Good code
    Tip 1

    View Slide

  11. Good code is a natural byproduct

    View Slide

  12. Twitter

    View Slide

  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('' + tweet + '');
    _this.reset();
    }
    })
    });

    View Slide

  14. How would I test this?

    View Slide

  15. I CAN’T!

    View Slide

  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('' + tweet + '');
    _this.reset();
    }
    })
    });
    Anonymous function
    HTML templating
    Hitting the server
    Tight coupling
    Nested callback

    View Slide

  17. “jQuery way”

    View Slide

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

    View Slide

  19. “If its hard to test its a good
    sign that you need to
    refactor your code”

    View Slide

  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('' + tweet + '');
    _this.reset();
    }
    })
    });
    Nested callback

    View Slide

  21. So whats the right way?

    View Slide

  22. Forces you to test
    through objects

    View Slide

  23. Leverage Objects to the max
    Tip 2

    View Slide

  24. View Slide

  25. Single Responsibility Principle

    View Slide

  26. Decoupled and intuitive

    View Slide

  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

    View Slide

  28. describe("An AJAX form", function() {
    var inst, $el;
    beforeEach(function() {
    $el = $('');
    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

    View Slide

  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

    View Slide

  30. How much time should I
    spend testing?

    View Slide

  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”

    View Slide

  32. What should I test?

    View Slide

  33. Your public API
    Tip 3

    View Slide

  34. How do I test my jQuery?

    View Slide

  35. By testing jQuery’s interface
    Tip 4

    View Slide

  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);
    });
    });
    });

    View Slide

  37. 1.Unit tests are a win
    2.JS objects are a win
    3.Only test your public API

    View Slide

  38. What next?

    View Slide

  39. Data-attributes

    View Slide

  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));
    }
    };

    View Slide

  41. Lifecycle hooks

    View Slide

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

    View Slide

  43. In action

    View Slide

  44. Wrap interface to plugins

    View Slide

  45. function Datepicker($el) {
    this.$el = $el;
    };
    Datepicker.prototype = {
    hide: function() {
    this.$el.datepicker('hide');
    },
    show: function() {
    this.$el.datepicker('show');
    }
    };

    View Slide

  46. Leverage mixins

    View Slide

  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);
    });

    View Slide

  48. {
    mustache

    View Slide

  49. Members only

    View Slide

  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);

    View Slide

  51. Thanks

    View Slide

  52. @damian
    damiannicholson.com

    View Slide

  53. Questions?

    View Slide