Ember Unit Testing

19-03-2014

As a bit of background I first started playing with Ember over a year ago, the API was unstable and Ember Data was still very much in flux. Fast forward to today and boy has a lot changed. Now Ember has just an amazing router, promises and things called outlets. I needed to catch up again.

In the more recent releases of Ember I noticed we have a whole section (1 page) dedicated to testing Ember applications. It uses a pretty neat syntax very similar to that of Capybara and provides an easy route to testing Ember applications. Lets be clear, these are integration tests. They poke from the outside, treat your app as a black box and assert against the DOM for expected outcomes.

I'm not against integration tests, far from it. However, I usually reserve them for acceptance criteria. What I mean is, if I have an aspect of an application that is particularly complex or vital in terms of value then I will create some kind of highly abstract test for it.

In short, I do not drive my design using integration tests. The reasons why are the usual suspects, slow and brittle.

Im not going to cover Ember integration tests here. They are well documented and pretty much all the articles on the web at the moment show you how to do what the documentation clearly tells you. Instead I'm going to focus on unit testing, particularly controllers. More particularly, controllers that interact and depend on Ember Data.

Unit Testing

For the examples I have just grabbed the starter kit and started building up from there.

On a side note I really like the look of the Ember App Kit and will probably start adopting some of its build steps down the line, maybe using Broccoli. But that's another blog post.

For now lets keep it simple, single file application with tests written using QUnit.

I should also say this post comes with a health warning. Im not convinced this is the correct way of unit testing controllers with Ember Data. But in the spirit of Cunningham's Law, for now it is gospel.

As an example we have a debate (very suitable at this time in Scotland), which has many points. If you imagine the view would be a list of existing points, a text area and a submit button. We enter text, hit submit and we see it added to the DOM. As a side effect we want to see the text area content be reset to no content. All straight forward stuff.

Our template would look like:

<script type="text/x-handlebars" data-template-name="session">
  <h2>DEBATE</h2>
  <ul>
  {{#each points}}
      <li>{{detail}}</li>
  {{/each}}
  </ul>
  {{textarea valueBinding='pointText'}}
  <button {{action 'createPoint'}}>Submit</button>
</script>

For completeness, the models in the application are:

App.Debate = DS.Model.extend({
  debateReference: DS.attr('string'),
  points: DS.hasMany('point', {async: true})
});

App.Point = DS.Model.extend({
  detail: DS.attr('string'),
  debate: DS.belongsTo('debate')
});

So lets actually write a test that will initialise the template with an empty string.

module("Debate Controller", {
  setup: function() {
    container = new Ember.Container();
    container.register('controller:debate', App.DebateController);
    controller = container.lookup('controller:debate');
  },

  teardown: function() {
    container.destroy();
  }
});

test("pointText", function() {
  equal('', controller.get('pointText'));
});

Whoah!! Now where did all that come from? And what is this Ember.Container?

Well, I was struggling just creating new controller objects in tests, so I reached out on Twitter for some guidance. Chad Hietala got back to me and linked to a post he had written on testing Ember applications which I found really helpful and when you are done reading this you should nip off and read that as well.

TL;DR Ember.Container allows us to inject the dependancies we need for a test. In this case we pull through an App.DebateController.

To make the test pass:

App.DebateController = Ember.ObjectController.extend({
  pointText: ''
});

Ember Data

Looking back at the template we can see that the button is tied to an action createPoint. As you would expect on clicking it, the point is added to the session, appears in the DOM and the textarea content is reset.

As we will be using Ember Data to persist points we have to consider it in our test. I played with many variations of injecting the store, but concluded that the only sane solution was to stub it. Here is how I achieved that:

var store = {
  createRecord: function(model, attrs) {
    obj = Ember.Object.create(attrs);
    obj.save = function() {
      obj.set('isSaved', true);
      return {
        then: function(callback) { return callback(obj); }
      };
    };
    return obj;
  }
};

It's pretty scary looking I admit but digging into it you can see a few design choices I have made.

Promises are not used. I basically duck type then so I don't need to consider the asynchronous behaviour of the store in my tests.

The other is I have explicitly created an isSaved property on the returned object for a bit of sugar when writing my tests.

The second test of this controller then looks like:

module("Debate Controller", {
  setup: function() {
    container = new Ember.Container();
    container.register('controller:debate', App.DebateController);
    controller = container.lookup('controller:debate');
    controller.set('store', store);
    controller.set('model', Ember.Object.create({ points: [] }));
  },

  teardown: function() {
    container.destroy();
  }
});

test("pointText", function() {
  equal('', controller.get('pointText'));
});

test('createPoint', function() {
  controller.set('pointText', 'Some Point');
  controller.send('createPoint');
  equal('', controller.get('pointText'));
  equal(1, controller.get('model.points.length'));
  ok(controller.get('model.points.firstObject.isSaved'));
});

Now I'm not sure about adding three assertions to a single test, it seems to be an OK thing to do in QUnit, if I'm wrong feel free to correct me on the twitter hashtag.

From this test (I added assertions sequentially on green) I was able to drive out the final controller code:

App.DebateController = Ember.ObjectController.extend({
  pointText: '',
  actions: {
    createPoint: function(emotion) {
      var newPoint = this.store.createRecord('point', {
        debate: this.get('model'),
        detail: this.get('pointText')
      });
      var controller = this;
      newPoint.save().then(function(point) {
        controller.set('pointText', '');
        controller.get('model.points').addObject(point);
      });
    }
  }
});

Wrapping Up

Im fairly happy with this as a solution to testing with Ember Data in the mix, but open to experiment. I guess what I like is at the moment I can see that setup for the test becoming a helper. Looking into the Ember Data source you can see a couple of utility functions setupStore and createStore. I think I may experiment with a similar approach down the line when I have gathered more feedback on my current solution.

Better still I hope I save someone the hours I put in trying too get something that should be simple up and running and productive.

To discuss, to make a point about or troll this post with me use the hashtag #bangline_ember.

References