Slide 1

Slide 1 text

Writing testable, scalable, maintainable, rock-solid JavaScript

Slide 2

Slide 2 text

How this can be achieved?

Slide 3

Slide 3 text

Hello I am @damian

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

uk.sageone.com

Slide 6

Slide 6 text

Scale & high growth

Slide 7

Slide 7 text

Sage One Platform

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

JavaScript + ? = Good code

Slide 10

Slide 10 text

JavaScript + Unit testing = Good code Tip 1

Slide 11

Slide 11 text

Good code is a natural byproduct

Slide 12

Slide 12 text

Twitter

Slide 13

Slide 13 text

$('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(); } }) });

    Slide 14

    Slide 14 text

    How would I test this?

    Slide 15

    Slide 15 text

    I CAN’T!

    Slide 16

    Slide 16 text

    $('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

    Slide 17

    Slide 17 text

    “jQuery way”

    Slide 18

    Slide 18 text

    1.AJAX 2.Event handling 3.DOM manipulation

    Slide 19

    Slide 19 text

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

    Slide 20

    Slide 20 text

    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

    Slide 21

    Slide 21 text

    So whats the right way?

    Slide 22

    Slide 22 text

    Forces you to test through objects

    Slide 23

    Slide 23 text

    Leverage Objects to the max Tip 2

    Slide 24

    Slide 24 text

    No content

    Slide 25

    Slide 25 text

    Single Responsibility Principle

    Slide 26

    Slide 26 text

    Decoupled and intuitive

    Slide 27

    Slide 27 text

    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

    Slide 28

    Slide 28 text

    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

    Slide 29

    Slide 29 text

    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

    Slide 30

    Slide 30 text

    How much time should I spend testing?

    Slide 31

    Slide 31 text

    “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”

    Slide 32

    Slide 32 text

    What should I test?

    Slide 33

    Slide 33 text

    Your public API Tip 3

    Slide 34

    Slide 34 text

    How do I test my jQuery?

    Slide 35

    Slide 35 text

    By testing jQuery’s interface Tip 4

    Slide 36

    Slide 36 text

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

    Slide 37

    Slide 37 text

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

    Slide 38

    Slide 38 text

    What next?

    Slide 39

    Slide 39 text

    Data-attributes

    Slide 40

    Slide 40 text

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

    Slide 41

    Slide 41 text

    Lifecycle hooks

    Slide 42

    Slide 42 text

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

    Slide 43

    Slide 43 text

    In action

    Slide 44

    Slide 44 text

    Wrap interface to plugins

    Slide 45

    Slide 45 text

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

    Slide 46

    Slide 46 text

    Leverage mixins

    Slide 47

    Slide 47 text

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

    Slide 48

    Slide 48 text

    { mustache

    Slide 49

    Slide 49 text

    Members only

    Slide 50

    Slide 50 text

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

    Slide 51

    Slide 51 text

    Thanks

    Slide 52

    Slide 52 text

    @damian damiannicholson.com

    Slide 53

    Slide 53 text

    Questions?