× {{alert.msg}} Never ask again
Receive New Tutorials
GET IT FREE

Learning Behavior-Driven Development with Ember CLI

– {{showDate(postTime)}}

Codementor Oliver Barnes has been coding with Ruby for over seven years, and he has been an avid practitioner of Test Driven Development and Behavior Driven Development. He has contributed to several open source projects such as Grape, and lately has been focusing on mentoring and pair programming.

This article was originally posted at his blog.


This tutorial walks through BDD‘ing a feature with Ember CLI, Mocha and Chai.

I’m writing it as I learn Emberjs, its way of doing things and its toolset, and try to adapt my usual workflow coming from Ruby. It’s meant as an initial guide for the RGSoC team working on Participate. Hopefully this post will be helpful to others learning Ember as well, and even better, invite people to show how they’re doing things themselves in the comments.

The feature I’ll build here in baby-steps will be Posting a New Initiative. In Participate, a user can post proposals for civic initiatives, to which other users can then suggest changes, and then vote on.

This first installment will involve nothing but filling out a simple form with a title and a description, and submitting it. The next installment will add validation checking both fields are filled-in.

As the feature gets incremented – an initiative must have an author and belong to an issue, for instance – new installments will come describing the process of adding them in. At some point I’ll talk about integrating with the separate API backend app.

Setup

Besides Ember CLI, Mocha and Chai, we’ll also use Emblem and EasyForm.

I go through installing all of them on another blog post.

Once you you got them and have generated your app (we’ll assume it’s called ‘post-initiative’ here in the tute), create a new file app/adapters/application.js and add this line to it:

export default DS.FixtureAdapter.extend();

This defines we’ll be using fixtures, so we don’t need to worry about the backend for now.

Starting with an acceptance test

Let’s start with a test that drives the interface, describing user interactions and expectations first, then implement these by letting test errors be the guide as much as possible.

Create a file named posting-an-initiative-test.js under tests/acceptance.

import startApp from 'post-initiative/tests/helpers/start-app';
import Resolver from 'post-initiative/tests/helpers/resolver';

var App;

suite('Posting an initiative', {
  setup: function(){
    App = startApp();
  },
  teardown: function() {
    Ember.run(App, 'destroy');
  }
});

Let’s add a test for a link to create a new initiative:

test('Successfully', function(){
  visit('/').then(function() {
    click( $("a:contains('Start a new initiative')") ).then(function() {
      expect(currentPath()).to.equal('/initiatives/new');
    });
  });
});

And, from the command line, run ember test:

post-initiative git:(simple-new-initiative) ✗ ember test
version: 0.0.37
Built project successfully. Stored in "/Users/work/Projects/post-initiative/tmp/class-tests_dist-Bv3r6aYr.tmp".
not ok 1 PhantomJS 1.9 - Posting an initiative Successfully
    ---
        message: >
            Error: Element [object Object] not found.

The key here is the message output. The opaque error means Jquery hasn’t found the link.

And understandably so, since it doesn’t exist yet.

Link to new initiative

Let’s implement it by adding it to application.emblem, under app/templates.

h2 Participate App

#menu
  = link-to 'initiatives.new' | Start a new initiative

=outlet

Run ember test again and you’ll get a new message:

message: >
    Assertion Failed: The attempt to link-to route 'initiatives.new' failed. The router did not find 'initiatives.new' in its possible routes: 'loading', 'error', 'index', 'application'

Route

In the router (app/router.js), let’s add a route to a new initiative resource:

Router.map(function() {
  this.resource('initiatives', function() {
    this.route('new');
  });
});

Tests should pass now.

1..1
# tests 1
# pass  1
# fail  0

# ok

This is the basic flow. Let’s add another expectation:

Adding the template and form for the new initiative

After clicking the link, the user should be able to fill in a title for the initiative. Add this line to the test

fillIn('div.title input', 'Public health clinic');

So it now looks like this:

test('Successfully', function(){
  visit('/').then(function() {
    click( $("a:contains('Start a new initiative')") ).then(function() {
      expect(currentURL()).to.equal('/initiatives/new');
      fillIn('div.initiative div.title input', 'Public health clinic')
    });
  });
});

Run the test again:

message: >
    Error: Element div.initiative div.title input not found.

To satisfy this, let’s create a template, and in it our form:

Create a directory initiatives under app/templates, and then add a file called new.emblem.

Paste the following in it:

form-for model
  = input title

Run the tests again, and they should pass.

Let’s add the remainder of the form-filling steps in our test:

visit('/').then(function() {
  click( $("a:contains('Start a new initiative')") ).then(function() {
    expect(currentURL()).to.equal('/initiatives/new');
    fillIn('div.title input', 'Public health clinic');
    fillIn('div.description textarea', 'Allocate compensation money to create a local public health clinic');
    click('form input[type=submit]');
  });

Running the tests again will give us:

message: >
    Error: Element div.description textarea not found.

Add the next input field and the save button to the form:

form-for controller
  = input title
  = input description as="text"
  = submit

The tests should now pass again.

Of course, submitting the form doesn’t do anything yet :)

Submitting the form

So what would a user expect to see after submitting the form. Likely she’ll:

  • Expect to see the url change
  • Expect to see the new initiative’s content so she can be sure it went through correctly.

She would also expect a confirmation message, but testing that is a little more involved from what I could find so far. So I’m leaving it for a later installment.

Let’s add these expectations within a then() function chained to click():

click('form input[type=submit]').then(function() {
  expect(currentPath()).to.equal('initiatives.show');
  expect(find('.title').text()).to.equal('Public health clinic');
  expect(find('.description').text()).to.equal('Allocate compensation money to create a local public health clinic');
});

then() returns a “promise”, and writing the expectations in a callback passed to it means they’ll get run once click() is done and the resulting rendering is finished. Promises can be a confusing concept at first (I’m still grokking them), but powerful – they let us not worry about all the issues coming from async events.

Run the tests:

message: >
    AssertionError: expected 'initiatives.new' to equal 'initiatives.show'

To satisfy this and get to the next error, we’ll need to take a few steps, inherent to how Ember wires routes and data being passed around. The errors I got when going through each of the steps weren’t very informative, and I got things working by trial & error & lot of googling and asking things on #emberjs. So I’m pragmatically breaking tdd here and just wiring enough to get to a useful error.

(For more info on what these steps are about, read the Ember guides on routing and controllers, and this thread on Discuss, which clarified things a lot for me. Ember’s architecture is still a moving target.)

First, let’s add this route handler for app/routes/initiatives/new.js:


import Ember from 'ember';

var InitiativesNewRoute = Ember.Route.extend({
  model: function() {
    return this.store.createRecord('initiative');
  },

  actions: {
    submit: function() {
      this.transitionTo('initiatives.show');
    }
  }
});

export default InitiativesNewRoute;

And this model definition (app/models/initiative.js) to go with it:

var Initiative = DS.Model.extend({
  title: DS.attr('string'),
  description: DS.attr('string')
});

export default Initiative;

Next, update the router (app/router.js) to include a path to /initiatives/show:

Router.map(function() {
  this.resource('initiatives', function() {
    this.route('new');
    this.route('show');
  });
});

And add the corresponding template (app/templates/initiatives/show.emblem). It can be empty for now.

Run the tests and we’ll get

AssertionError: expected '' to equal 'Public health clinic'

Which means that we got the route transition to work, and are now looking at this test:

expect(find('.title').text()).to.equal('Public health clinic');

We made some progress. So far the user can:

  • navigate to our app’s homepage
  • click on the link for a new initiative
  • fill in a form with the title and description for it
  • submit it
  • get redirected to the created initiative page.

But there’s no initiative created yet. Let’s tackle this next:

Handling the form submission

Here I’m also going to wire a few things up to get to the next test error.

Let’s update InitiativesNewRoute to handle the submitted params, and then transition to /initiatives/show/:initiative_id

var InitiativesNewRoute = Ember.Route.extend({
  model: function(params) {
    return this.store.createRecord('initiative', params);
  },

  actions: {
    submit: function() {
      var _this = this;
      var initiative = this.get('controller.model');
      initiative.save().then(function(model) {
        _this.transitionTo('initiatives.show', model.get('id'));
      });
    }
  }
});

Update the router to accept the :initiative_id segment:

this.resource('initiatives', function() {
  this.route('new');
  this.route('show', {path: '/:initiative_id'});
});

Create a InitiativesShowRoute (app/routes/initiatives/show.js) to fetch the initiative model:

import Ember from 'ember';

var InitiativesShowRoute = Ember.Route.extend({
  model: function(params) {
    return this.store.find('initiative', params.initiative_id);
  }
});

export default InitiativesShowRoute;

And, finally, a new template for showing the initiative (app/templates/initiatives/show.emblem)

.title
  h2
    model.title

.description
  p
    model.description

Run the tests and they should pass.

Start up ember-cli’s server by running ember serve, and point your browser to http://localhost:4200/, for a good sanity check. If everything went well, this tiny feature should just work :)


 

Oliver BarnesNeed Oliver’s help? Book a 1-on-1 session!

View Oliver’s Profile

or join us as an expert mentor!




Questions about this tutorial?  Get Live 1:1 help from Ember.js experts!
Aswin Murugesh
Aswin Murugesh
5.0
Passionate techie who enjoys coding
Programmer and a Technical lead with hands on experience in Python, Javascript, Django for the past 5 years. I also have a good amount of DevOps...
Hire this Expert
Michael Mayernick
Michael Mayernick
5.0
Friendly, patient coach for Ruby, Python, JavaScript, Go, Data Science, Big Data, AI, Blockchain, Chatbots, React / Angular / Native. Technical co-founder of VC backed big data startup.
Hey all - I've been developing web applications for 12 years and love teaching, whether it is your first time, you're looking to pick up a new...
Hire this Expert
comments powered by Disqus