Unit Testing Ember

Introduction

In my last post, I introduced you to testing JavaScript code with QUnit. Today I’ll be walking you through another example, but this time we’ll be using Ember.js as our framework. We’ll be building a small contact management application from the ground up using TDD along the way. Let’s get started!

New Ember Application

We’ll start by creating a new Ember.js application. We’ll do this using Ember-CLI, the preferred way to start a new Ember application (per the Emberjs.com website, Figure 01)

Figure 01: Snippet of the Ember Homepage
Figure 01: Snippet of the Ember Homepage

To get started with Ember-CLI, you’ll need Node.js and NPM installed, it’s as simple as going to the Node website and clicking the big Install button on their homepage. Easy-peasy. Then it’s simply two commands:

npm install -g ember-cli
ember new contacts

Then change into the contacts directory. Open that folder in an editor of your choice.

Finally, run the command ember serve to have your app served up on port 4200. Navigate to http://127.0.0.1:4200, and you should see a simple “Welcome to Ember.js” message.

Running Your Tests

You can either navigate to http://127.0.0.1:4200/tests to see your tests, or you can run the ember test command. If you want, you can also run a server that will execute your tests, that command is ember test --serve.

Testing Models

Like my last post where I used TDD to walk you through testing a JavaScript function, we’ll do the same here, but testing Ember. Let’s start by adding a contact model. We can generate the model and a unit test by using Ember-CLI’s awesome generators:

ember g model Contact firstName:string lastName:string

This generates contact.js (under app/models) and a unit test (under tests/unit/models/contact-test.js). Jump into the test file (tests/unit/models/contact-test.js) and we’ll start by test driving a computed property on our contact. We’re going to want a fullName property on our contact model. Let’s write a test to get started, ensuring that fullName computes properly. We also want to make sure that it updates if we change the firstName or lastName. Put this at the end of the contact-test.js file. The test file already has one simple test in it, but the important part of the test is the moduleForModel call. This is what allows us to test Ember Data models easily.

moduleForModel('contact', 'Contact Model', {
  // Specify the other units that are required for this test.
  needs: []
});

With the moduleForModel block in place, we can now write our tests. Be sure to also import Ember from 'ember'; since we’ll be using Ember.run in our tests. moduleForModel takes the name of the model, a description, and a object of requirements.

test('fullName', function(assert){
  var contact = this.subject({firstName: 'Damien', lastName: 'White'});
  assert.equal(contact.get('fullName'), 'Damien White');

  Ember.run(function() {
    contact.set('firstName', 'Dave');
  });

  assert.equal(contact.get('fullName'), 'Dave White');

  Ember.run(function() {
    contact.set('lastName', 'Marini');
  });

  assert.equal(contact.get('fullName'), 'Dave Marini');
});

There are 3 assertions in this test. You could break these up into 3 separate tests if you want, but since these are all related, I put them in one test. Most of this should be readable, but the Ember.run calls might be confusing here. The reasoning for this is that our Ember tests are asynchronous. If we don’t wrap the contact.set calls, we’ll get an error. When the app is in testing mode, automatic invocation of Ember.run is disabled. Because of this, anytime we are creating or changing a model with bound or observed properties, we need to wrap them in Ember.run. Meaning let the run loop complete before working against that property.

The first assertion checks to make sure that fullName works, given our subject. The second, makes sure that the firstName triggers a change for the fullName. The final assertion does the same as the second, but for the lastName. The code to make this test pass is as follows:

export default DS.Model.extend({
  ...
  fullName: (function() {
    return this.get('firstName') + ' ' + this.get('lastName');
  }).property('firstName', 'lastName')
});

Testing Routes

Let’s now work on a route. We’re going to want to list all the contacts on a route called contacts. Turning to Ember-CLI’s generators, we can generate a route using the code: ember g route contacts. This will create three files for us:

  1. app/routes/contacts.js
  2. app/templates/contacts.hbs
  3. tests/unit/routes/contacts-test.js

Since we’re using TDD, hop into the tests/unit/routes/contacts-test.js file. The initial file contains imports and the key part of testing routes, moduleFor.

moduleFor('route:contacts', 'Contacts Route', {
  // Specify the other units that are required for this test.
  // needs: ['controller:foo']
});

Like moduleForModel, moduleFor takes the name of the module, a description, and a object of requirements. I have added a description of “Contacts Route” to make our tests a little bit friendlier. We can now write a test for our route. We’re using Ember-Data so I know that our route is going to be using the Ember-Data store. Since we aren’t testing the store, we’ll mock the object in the test. Jump into tests/unit/routes/contacts-test.js as we’ll put our tests there

test('it returns a list of contacts', function(assert) {
  var store = {
    find: function(type) {
      return new Ember.RSVP.Promise(function(resolve) {
        resolve([
          {id: 1, firstName: 'Damien', lastName: 'White'},
          {id: 2, firstName: 'Dave', lastName: 'Marini'}
        ]);
      });
    }
  };
  var route = this.subject();
  route.set('store', store);
  assert.deepEqual(route.model()._result, [
    {id: 1, firstName: 'Damien', lastName: 'White'},
    {id: 2, firstName: 'Dave', lastName: 'Marini'}
  ]);
});

Ember-Data’s find method returns a promise, so we’ll emulate that behavior in our mock store. From there we can check the _result of the model (remember, it’s a promise) and do a deepEqual assertion to make sure it returns the list we expect.

Finally, here’s the code to make our test pass (in app/routes/contacts.js).

import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return this.store.find('contact');
  }
});

Testing Controllers

Continuing with our testing, let’s see how we’d test a controller. We’ll use Ember CLI to generate our contacts controller, using the command: ember g controller contacts. This generates two files for us.

  1. app/controllers/contacts.js
  2. tests/unit/controllers/contacts-test.js

You know the drill by now, we’ll get started in the test file tests/unit/controllers/contacts-test.js. We want to test that our controller is an ArrayController.

To start testing a controller, we need to use moduleFor again.

moduleFor('controller:contacts', 'Contacts Controller', {
  // Specify the other units that are required for this test.
  // needs: ['controller:foo']
});

Now, let’s write our test. We’ll simply test if there is a method addObject, which only exists on the ArrayController.

test('it is an ArrayController', function(assert) {
  var controller = this.subject();
  assert.ok(controller.addObject, 'Expected addObject method to exist');
});

And the simple code to make it pass…

import Ember from 'ember';

export default Ember.ArrayController.extend({
});

Since we’re testing controllers, let’s look how we’d test an action. We need to create an action to set a flag of whether or not all of the contacts are selected. We’ll put a property on the controller called selectAll and we’ll create an action called toggleSelectAll.

test('calling the action toggleSelectAll updates selectAll' ,function(assert) {
  assert.expect(3);

  var controller = this.subject();

  // check the value before calling the action
  assert.equal(controller.get('selectAll'), false);

  // now call the action
  controller.send('toggleSelectAll');

  // Check the new value
  assert.equal(controller.get('selectAll'), true);

  // call the action again
  controller.send('toggleSelectAll');

  // we should be back to false
  assert.equal(controller.get('selectAll'), false);
});

We’ll tell QUnit that we expect three assertions. This is a way to make sure more or less things sneak into your tests. It’s not required, but I figured I show you some additional commands you can use. Then we make sure the default value is false and once we call toggleSelectAll, we check to ensure the value is true. We call the action one more time to make sure it actually toggles the value. Now for the controller code…

export default Ember.ArrayController.extend({
  selectAll: false,

  actions: {
    toggleSelectAll: function() {
      this.set('selectAll', !this.get('selectAll'));
    }
  }
});

Conclusion

Through this post, you learned how to unit test a model, the router, and a controller. In my next post, I’ll show you how to do integration and acceptance testing, where we check the full stack instead of pieces in isolation. Both testing approaches are important to having quality code. I encourage you to check out the official testing documentation as there are more examples to help you.