Improving Ember-CLI Testing with Sinon.js

When I first started developing single page web apps using Ember-CLI, I was very impressed with the testing tools they provide out of the box. However, after I spent more time in an Ember-based tech stack, I found myself wanting a better way to mock objects and write expectations.

In this blog post I’ll explain how an Ember developer using Ember-CLI can install and use the popular testing testing tool Sinon.js to write better tests and ensure a more correct Ember app.

Ember / Sinon.js Setup

I’ve created a Github repository here with the code from this post. Also, since the recommendation these days is for all new Ember projects to use Ember-CLI, I will assume you’re using it.

First, we create the app itself:

ember new app-with-sinon

Next we need to bring in Sinon. Fortunately, there is a convenient Ember-CLI addon available:

npm install --save-dev ember-sinon
ember g ember-sinon

Sinon.js will now be available via the global sinon variable in your tests.

However, if you use that variable, your test file will fail the out-of-the-box JSHint configuration. To remedy that problem, open app-with-sinon/app/tests/.jshintrc and add "sinon" to the "predef" array.

Now, restart your Ember server with ember s and re-run the tests. They should pass.

Using Sinon.js in Ember with Mocha Testing

The above approach works well with Qunit, but I personally prefer to use Mocha with the Chai matchers via the ember-mocha package. Unfortunately I ran into some trouble using ember-sinon, so I added Sinon.js with Bower, as follows:

bower install http://sinonjs.org/releases/sinon-1.12.2.js

That brings Sinon in via Bower, but you’ll need to modify your app’s Brocfile.js using the approach outlined in Ember-CLI’s managing dependencies documentation. Here is what I added to my Brocfile to merge Sinon into the vendor assets in test mode:

var isProduction = EmberApp.env() === 'production';
if ( !isProduction ) {
    app.import( app.bowerDirectory + '/sinon/index.js', { type: 'test' } );
}

// ... other stuff ...
module.exports = app.toTree();

Testing Actions with Sinon.js

Sinon provides a number of useful mock and stub helpers, but what I’ve found most useful in my Ember development are test spies. Test spies record function calls that happened, including their arguments, return type, the value of this, and the exceptions that were raised (if any).

Test spies have many uses but the most obvious need in an Ember app is to improve testing of actions. Actions are events that can be linked to an event in the UI (e.g., on a button click), but they are also the mechanism that allows components to communicate using action bubbling. An example of action bubbling occurs when a component within a controller (or other component) calls sendAction.

Example Code

In my test app I will be writing a very simple “phone” interface using a component for each button on the keypad. Buttons are given ids and when they are pressed, they send their ID up to the controller that contains all buttons. The controller appends the button ids and then “makes a phone call.” Since this is a simple example, I will be working in the Application controller directly for the “keypad,” so create a controller for it:

ember g controller application

Now, create the button component:

ember g component button-with-id

In Application.hbs, add the following:

Terrible Phone

{{currentNumber}} {{#each button-group in buttonGroups}}

{{#each button-id in button-group}}

{{button-with-id id=button-id sendId=’receiveButtonPress’}}

{{/each}}

{{/each}}

In the above code snippet, we can see that for each button-id in the button-group array, we create a button-with-id</code component and set its sendId action to the controller's receiveButtonPress action. Put another way, when the component calls sendAction('sendId'), the controller's receiveButtonPress action will be called. We will be testing this behavior with Sinon.

And, Application.js should look like this:

import Ember from 'ember';

export default Ember.Controller.extend({
  buttonGroups: Ember.A([
    ['1', '2', '3'],
    ['4', '5', '6'],
    ['7', '8', '9'],
    ['*', '0', '#'] ]),

  currentNumber: '',

  clearNumber: function() {
    this.set('currentNumber', '');
  },

  dialNumber: function() {
    // Dial a number
    this.clearNumber();
  },

  actions: {
    receiveButtonPress: function(buttonId) {
      this.set('currentNumber', this.get('currentNumber') + buttonId);

      if (this.get('currentNumber').length === 10) {
        this.dialNumber();
      }
    }
  }
});

The two remaining pieces are the template for the button-with-id component and its javascript. In button-with-id.hbs:


And the button-with-id.js:

import Ember from 'ember';

export default Ember.Component.extend({

  actions: {
    buttonPressed: function() {
      this.sendAction('sendId', this.get('id'));
    }
  }
});

Tests

First, I’d like to test that the button-with-id component sends its sendId action with the correct data when buttonPressed occurs. We can do this very nicely with Sinon!

Open button-with-id-test.js. The test looks like this:

test('it sends its id when the button is pressed', function(assert) {
  var component = this.subject();
  var buttonId = '7';
  component.set('id', buttonId);

  component.sendAction = sinon.spy();

  component.send('buttonPressed');

  assert.ok(component.sendAction.calledOnce);
  assert.ok(component.sendAction.calledWith('sendId', buttonId));
});

What we’re doing in this test is sending buttonPressed event directly to the component then asserting that the component:

  1. Called sendAction once.
  2. Called sendAction with the component’s id parameter.

While this test case is admittedly a little contrived, in a real-world Ember app it is possible to have more complex branching behaviors in actions. Having Sinon in the toolbox really helps a developer write tests and have confidence that the code is correct.

We also want to test the controller’s behavior when it receives the button press for a button component. Open application-test.js:

test('it makes a call when it has received 11 digits', function(assert) {
  var controller = this.subject();
  controller.dialNumber = sinon.spy();
  controller.set('currentNumber', '1555867530');

  controller.send('receiveButtonPress', '9');
  assert.ok(controller.dialNumber.calledOnce);
});

This is similar to the component test, except here we test that the controller “makes a phone call” by calling dialNumber when it has received 11 digits (admittedly, it’s a pretty crappy phone …). We set the spy to watch controller.dialNumber, set up the controller to have 10 digits, then programmatically send the final button press action. Sinon lets us easily prove that dialNumber was called.

In this post I have barely scratched the surface of how Sinon.js can be used. As I mentioned at the top, the code used in this post can be found here. I’d love to hear your other uses for Sinon!

 
Conversation
  • David Froger says:

    Sinon spy looks like Python mock! :-) (https://docs.python.org/3/library/unittest.mock.html)

    Great lib!

  • Ben says:

    With component testing deprecating this.subject(), have you been able to change your approach and still use sinon? If so, how are you doing this?

    • John Fisher John Fisher says:

      Hi Ben,

      This deprecation was news to me, however it looks like it only applies to integration tests, so you can at least continue to use this approach with unit tests:

      “People can upgrade gracefully by just ensuring their existing tests have a needs option (or integration:false or unit:true, depending on which of those we decide to support), which will trigger the existing unit test behavior.”

      https://github.com/switchfly/ember-test-helpers/issues/25#issuecomment-90712488

  • Comments are closed.