Skip to content

Instantly share code, notes, and snippets.

@oliverbarnes
Last active August 29, 2015 14:04
Show Gist options
  • Save oliverbarnes/2eb34d33cc329d1bc057 to your computer and use it in GitHub Desktop.
Save oliverbarnes/2eb34d33cc329d1bc057 to your computer and use it in GitHub Desktop.
Learning Behavior-Driven Development with Ember CLI - part I
This tutorial walks through [BDD](http://en.wikipedia.org/wiki/Behavior-driven_development)'ing a feature with [Ember CLI](http://iamstef.net/ember-cli/), [Mocha](http://visionmedia.github.io/mocha/) and [Chai](http://chaijs.com).
I'm writing it as I learn [Emberjs](http://emberjs.com), 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](http://railsgirlssummerofcode.org) [team](http://rghelsinki2014.tumblr.com) working on [Participate](https://github.com/oliverbarnes/participate-frontend). 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](https://github.com/oliverbarnes/participate).
### Setup
Besides Ember CLI, Mocha and Chai, we'll also use [Emblem](http://emblemjs.com) and [EasyForm](https://github.com/dockyard/ember-easyForm).
I go through how to get up and running with them [on another blog post](http://olibarnesdevstuff.tumblr.com/post/93398799975/getting-setup-with-ember-cli-emblem-easyform-mocha).
Once you're setup 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:
```javascript
export default DS.FixtureAdapter.extend();
```
This defines we'll be using [fixtures](http://cubicleapps.com/articles/todo-mvc-with-ember-cli-part-2#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.
```javascript
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:
```javascript
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
```javascript
fillIn('div.title input', 'Public health clinic');
```
So it now looks like this:
```javascript
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:
```javascript
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:
```javascript
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:
```javascript
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](https://github.com/aexmachina/ember-notify/issues/7#issuecomment-49848718) 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()`:
```javascript
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"](http://emberjs.com/api/classes/Ember.RSVP.Promise.html), 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](http://emberjs.com/guides/routing/) and [controllers](http://emberjs.com/guides/controllers/), and this [thread on Discuss](http://discuss.emberjs.com/t/where-should-i-define-a-save-action/5062), 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:
```javascript
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:
```javascript
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:
```javascript
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:
```javascript
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
```javascript
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:
```javascript
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:
```javascript
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 :)
Coming soon: Adding validations
> Written with [StackEdit](https://stackedit.io/).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment