Reclaiming discipline in client side web development

James Roper

Outline

Dependency management

Dependency management

libraryDependencies ++= Seq(
  "org.webjars" % "bootstrap" % "3.1.0",
  "org.webjars" % "knockout" % "3.0.0",
  "org.webjars" % "leaflet" % "0.7.1"
)

Dependency management

<script type="text/javascript" 
  src="@routes.WebJarAssets.at(
    WebJarAssets.locate("js/bootstrap.min.js")
  )"></script>

Dependency management

<link rel="stylesheet" media="screen"
  href="@routes.WebJarAssets.at(
    WebJarAssets.locate("css/bootstrap.min.css")
  )"/>

Dependency injection

Dependency injection

define(function() {
  return {
    name: "duck",
    speak: function() {
      console.log("quack quack");
    }
  };
});

Dependency injection

require(["duck"], function(duck) {
  console.log("What does a " + duck.name + " do?");
  duck.speak();
});

Dependency injection

define(["duck", "pig"], function(duck, pig) {
  return {
    names: [duck.name, pig.name],
    commotion: function() {
      duck.speak();
      pig.speak();
    }
  };
});

Dependency injection

require.config({
  paths: {
    farm: "./farm",
    duck: "./animals/duck",
    pig: "./animals/pig"
  }
});
require(["farm"], function(farm) {
  farm.commotion();
});

Dependency injection

<script type="text/javascript" 
  src="@routes.WebJarAssets.at(
    WebJarAssets.locate("require.js")
  )"
  data-main="@routes.Assets.at("main.js")"></script>

Dependency injection

require.config({
  paths: {
    map: "./models/map"
  },
  map: {
    "*": {
      leaflet: "webjars!leaflet.js"
    }
  }
});
require(["map"], function(map) {
  map.init();
});

Dependency injection

<script type="text/javascript" 
  src="@routes.WebJarAssets.requirejs"
  data-main="@routes.Assets.at("main.js")"></script>

Separation of concerns

Makes code easier to comprehend and test

What to separate

AJAX - with promises

What to separate

DOM manipulation, using templates, a framework or with manual encapsulation

What to separate

Browser APIs, such as history, storage, WebSockets, EventSource

What to separate

... plus anything else that would otherwise be mixing concerns

Testing

Mocha testing

var assert = require("assert");
describe("Adder", function() {
  describe("#addFour", function() {
    it("should add 4", function() {
      assert.equal(5, addFour(1));
    });
  });
});

Mocha testing

var assert = require("assert");
var requirejs = require("requirejs");
describe("Adder", function() {
  var adder = requirejs("./adder");
  describe("#addFour", function() {
    it("should add 4", function() {
      assert.equal(5, adder.addFour(1));
    });
  });
});

Mocha testing

But what about mocking?

Mocking

var assert = require("assert");
var requirejs = require("requirejs");
var Squire = requirejs("./squire");
describe("Adder", function() {
  describe("#addFour", function() {
    it("should add 4", function() {
      var injector = new Squire();
      injector.require(["./adder"], function(adder) {
        assert.equal(5, adder.addFour(1));
      });
    });
  });
});

Mocking

describe("Adder", function() {
  describe("#addFour", function() {
    it("should add 4", function(done) {
      var injector = new Squire();
      injector.require(["./adder"], function(adder) {
        assert.equal(5, adder.addFour(1));
        done();
      });
    });
  });
});

Mocking

var MockAnimal = function(name) {
  this.name = name;
  this.spoken = 0;
  self = this;
  this.speak = function() {
    self.spoken += 1;
  }
};

Mocking

it("should return all names", function(done) {
  var injector = new Squire();
  injector.mock("duck", new MockAnimal("Mock duck"));
  injector.mock("pig", new MockAnimal("Mock pig"));
  injector.require(["./farm"], function(farm) {
    assert.deepEqual(["Mock duck", "Mock pig"],
        farm.names);
    done();
  });
});

Mocking

it("should cause a commotion", function(done) {
  var injector = new Squire();
  var duck = new MockAnimal("Mock duck");
  var pig = new MockAnimal("Mock pig");
  injector.mock("duck", duck);
  injector.mock("pig", pig);
  injector.require(["./farm"], function(farm) {
    farm.commotion();
    assert.equal(1, duck.spoken);
    assert.equal(1, pig.spoken);
    done();
  });
});

Danger!

The "but these are only tests" anti-pattern

DRY

function withFarm(test) {
  return function(done) {
    var injector = new Squire();
    var duck = new MockAnimal("Mock duck");
    var pig = new MockAnimal("Mock pig");
    injector.mock("duck", duck);
    injector.mock("pig", pig);
    injector.require(["./farm"], function(farm) {
      test(farm, duck, pig);
      done();
    });
  };
});

DRY

it("should return all names", withFarm(farm) {
  assert.deepEqual(["Mock duck", "Mock pig"],
      farm.names);
});
it("should cause a commotion",
    withFarm(farm, duck, pig) {
  farm.commotion();
  assert.equal(1, duck.spoken);
  assert.equal(1, pig.spoken);
});

Thankyou