Salt Lake City, UT

Agenda

Things we'll cover:

  • Setting up Ember App for Testing
  • Integration Tests
  • Custom Test Helpers
  • Testing XMLHttpRequests (XHR)
  • Testing Handlebars Helpers
  • Testing Routes
  • Unit Testing

Setup

Preparing your Ember App for Testing

Setup

index.html

<html>
<body>
  <!-- Handlebars templates go here -->

  <script src="js/libs/jquery-1.10.2.js"></script>
  <script src="js/libs/handlebars-1.1.2.js"></script>
  <script src="js/libs/ember-1.3.1.js"></script>
  <script src="js/app.js"></script>

  <!-- to activate the test runner, add the "?test" query string parameter -->
  <script src="tests/runner.js"></script>
</body>
</html>

Setup

tests/runner.js

if (window.location.search.indexOf("?test") !== -1) {
  document.write(
    '<div id="qunit"></div>' +
    '<div id="qunit-fixture"></div>' +
    '<div id="ember-testing-container">' +
    '  <div id="ember-testing"></div>' +
    '</div>' +
    '<link rel="stylesheet" href="tests/runner.css">' +
    '<link rel="stylesheet" href="tests/vendor/qunit-1.12.0.css">' +
    '<script src="tests/vendor/qunit-1.12.0.js"></script>' +
    '<script src="tests/tests.js"></script>'
  )
}

Setup

tests/tests.js

// in order to see the app running inside the QUnit runner
App.rootElement = '#ember-testing';

// defer readiness of application and set router location to `none`
App.setupForTesting();

// inject test helpers into window's scope
App.injectTestHelpers();

Setup

Hello Test!

test("i kan pazz", function() {
  ok(true, "Hello Test!");
});

Integration Tests

Testing from the Outside In

Integration Tests

What are integration tests?

  • They are generally used to test important work flows within your application.
  • They require the application to be running (in test mode)
  • They emulate user interaction and confirms expected results

Integration Tests

Built-in Test Helpers

Integration Tests

TDD Time!

Integration Tests

TDD Results

test("show list of contacts", function() {
  expect(2)

  visit("/");
  andThen(function() {
    shouldHaveElementWithCount("ul li", 3);
    equal(find("ul li:first").text(), "Ryan", "Ryan is the first contact");
  });
});

test("add contact to list", function() {
  expect(3);

  visit("/");
  addContact("Jason");
  andThen(function() {
    shouldHaveElementWithCount("ul li", 4);
    equal(find("ul li:last").text(), "Jason", "Jason is the last contact");
    equal(find("#name").val(), "", "Form has been reset");
  });
});

Integration Tests

TDD Results

<script type="text/x-handlebars" id="index">
  <form {{action addContact on="submit"}}>
    {{input type="text" value=name id="name" placeholder="Type Name..."}}
    <button class="create">Add Contact</button>
  </form>

  <ul>
  {{#each item in model}}
    <li>{{item}}</li>
  {{/each}}
  </ul>
</script>

Integration Tests

TDD Results

App.IndexRoute = Ember.Route.extend({
  model: function() {
    return ['Ryan', 'Stanley', 'Eric'];
  }
});

App.IndexController = Ember.ArrayController.extend({
  actions: {
    addContact: function() {
      var name = this.get("name");
      this.pushObject(name);
      this.set("name", null);
    }
  }
});

Custom Test Helpers

Creating your own test helpers

Custom Test Helpers

Ember.Test.registerHelper

Helpers are injected when App.injectTestHelpers() is called.

Ember.Test.registerHelper('shouldHaveElementWithCount', 
  function(app, selector, n, context) {
    var el = findWithAssert(selector, context);
    var count = el.length;
    equal(n, count, "found " + count + " times");
  }
);

// shouldHaveElementWithCount("ul li", 3);

Custom Test Helpers

Ember.Test.registerAsyncHelper

Async Helpers will not run until prior async helpers complete.

Ember.Test.registerAsyncHelper('addContact',
  function(app, name, context) {
    fillIn("#name", name);
    click("button.create");
  }
);

// addContact("Dan");
// addContact("Deric");

Testing XMLHttpRequests

Mocking server responses

Testing XMLHttpRequests

This works...

App.Contact = Ember.Object.extend({});
App.Contact.reopenClass({
  find: function() {
    var url = "http://addressbook-api.herokuapp.com/contacts",
        contacts = Em.A([]);

    Ember.$.getJSON(url).then(function(data) {
      data.contacts.forEach(function(c) {
        var contact = App.Contact.create(c);
        contacts.pushObject(contact);
      }.bind(this));
    }.bind(this));

    return contacts;
  }
});

Testing XMLHttpRequests

... until you want to perform tests


  test("contact list appears on root url", function() {
    expect(2);
    visit("/");
    andThen(function() {
      shouldHaveElementWithCount("ul li", 3);
      equal(find("ul li:first").text(), "Ryan", "Ryan is the first contact");
    });
  });

Testing XMLHttpRequests

Wrap Ember.$.ajax callbacks with Ember.run

function ajax(url, options) {
  return new Ember.RSVP.Promise(function(resolve, reject){
    options = options || {};
    options.url = url;

    options.success = function(data) {
      Ember.run(null, resolve, data);
    };

    options.error = function(jqxhr, status, something) {
      Ember.run(null, reject, arguments);
    };

    Ember.$.ajax(options);
  });
}

Testing XMLHttpRequests

Wrap Ember.$.ajax callbacks with Ember.run

Testing XMLHttpRequests

ic-ajax

ic-ajax is an Ember-friendly jQuery.ajax wrapper

  • returns RSVP promises
  • makes apps more testable (resolves promises with Ember.run)
  • makes testing ajax simpler with fixture support

however, there are two exceptions which make it different

  • success and error callbacks are not supported
  • does not resolve three arguments like $.ajax (see next slide)

Testing XMLHttpRequests

ic-ajax

var ajax = ic.ajax;
App.ApplicationRoute = Ember.Route.extend({
  model: function() {
    return ajax('/foo');
  }
}

// if you need access to the jqXHR or textStatus, use raw
ajax.raw('/foo').then(function(result) {
  // result.response
  // result.textStatus
  // result.jqXHR
});

Testing XMLHttpRequests

ic-ajax - Example Usage

ic.ajax.defineFixture('api/v1/courses', {
  response: [{name: 'basket weaving'}],
  jqXHR: {},
  textStatus: 'success'
});

ic.ajax('api/v1/courses').then(function(result) {
  deepEqual(result, ic.ajax.lookupFixture('api/v1/courses').response);
});

Testing XMLHttpRequests

ic-ajax

module("Integration - Contacts", {
  setup: function() {
    App.reset();
    ajax.defineFixture("http://addressbook-api.herokuapp.com/contacts", {
      response: { 
        contacts: [
          { first: "Bob" },
          { first: "Jason" },
          { first: "Dan" }
        ]
      },
      jqXHR: {},
      textStatus: "success"
    });
  },
  teardown: function() {}
});
...

Testing XMLHttpRequests

ic-ajax

...
test("contact list appears on root url", function() {
  expect(2);

  visit("/");

  andThen(function() {
    shouldHaveElementWithCount("ul li", 3);
    equal(find("ul li:first").text(), "Bob", "Bob is the first contact");
  });
});

Testing XMLHttpRequests

ic-ajax

Testing Handlebars Helpers

Testing Handlebars Helpers

Example

Ember.Handlebars.helper('upcase', function(value) {
  return value.toUpperCase();
});
{{#each person in controller}}
  <li>{{upcase person.name}}</li>
{{/each}}

Testing Handlebars Helpers

What Would Robert Do (WWRD)?

function createView(template, context){
  if (!context) { context = {}; }
  var View = Ember.View.extend({
    controller: context,
    template: Ember.Handlebars.compile(template)
  });
  return View.create();
}

function append(view){
  Ember.run(function(){
    view.appendTo('#qunit-fixture');
  });
}

Testing Handlebars Helpers

What Would Robert Do (WWRD)?

module("Handlebars Helpers");

test('upcase', function(){
  var view = createView("{{upcase 'something'}}");
  append(view);

  var renderedText = view.$().text();
  equal(renderedText, 'SOMETHING', "Text rendered: " + renderedText);
});

Testing Routes

with some sprinkles of sinon.js

Testing Routes

var route, expectedModel;

module("Unit: Index Route", {
  setup: function() {
    App.reset();
    route = App.IndexRoute.create();
  },
  teardown: function() {
    Ember.run(route, 'destroy');
  }
});

test("it exists", function() {
  expect(2);

  ok(route);
  ok(route instanceof Ember.Route);
});

Testing Routes

test("#model", sinon.test(function() {
  expect(2);

  var stub,
      expectedModel = [
        { first: "Jon" }
      ];

  stub = sinon.stub(App.Contact, "find").returns(expectedModel);

  equal(route.model(), expectedModel, "model is correct");
  ok(stub.called, "App.Attendee.find() was called");
}));

Unit Testing

It's coming...

Thank You!

Feel free to contact me at any of the following: