Last active
August 29, 2015 14:04
-
-
Save oliverbarnes/2eb34d33cc329d1bc057 to your computer and use it in GitHub Desktop.
Learning Behavior-Driven Development with Ember CLI - part I
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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