EmberJS Testing On Rails
EmberJS Testing On Rails
js - Testing on Rails
Martin Feckie
This book is for sale at http://leanpub.com/emberjs-testingonrails
This version was published on 2014-05-05
This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishing
process. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools and
many iterations to get reader feedback, pivot until you have the right book and build traction once
you do.
2014 Martin Feckie - RN, MHSM
Contents
Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Erratum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Introduction . . . . . . . . . . . . . .
A Bit About My Motivation . . . .
First There Was Rails . . . . . . . .
Along Came jQuery . . . . . . . . .
Javascript IV -Ember - A New Hope
Testing to the Rescue . . . . . . . .
.
.
.
.
.
.
4
4
5
5
6
7
Design Choices, the Golden Path and Why Didnt You Include / Use / Show X, Y, Z? . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
17
17
19
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9
9
9
9
10
12
13
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
24
24
27
28
30
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
32
33
35
37
CONTENTS
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
39
40
41
41
42
43
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
45
45
50
51
52
54
62
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
66
67
68
69
71
72
74
75
76
80
82
86
86
89
94
96
99
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Acknowledgements
I would like to acknowledge the special contribution of Javier Cadiz for the extremely useful feedback
and for test driving the book.
The Ruby Rogues and their amazing guests. Ive consumed every episode (many more than once)
and have even listened to them whilst snowboarding! Their content is constantly excellent.
The JavaScript Jabber podcast for more wonderful topics and guests.
Douglas Crockfords series Crockford on JavaScript should be watched by anyone interested in
the language, entertaining, insightful and really brilliant at explaining the good bits and the bits to
worry about.
Sandi Metz for Practical Object Oriented Design in Ruby and every one of her talks available on the
internet.
Ryan Bates for the RailsCasts series. Giving such a great resource away is a true act of altruism.
http://www.yuiblog.com/crockford/
Erratum
Chapter 7
Contact class should destroy emails when contact is destroyed.
contact.rb
1
2
3
Chapter 7
Serializer should not include has_one :contact
email_serializer.rb
1
2
3
Chapter 7
contact_id was missing from model.
New file - /app/assets/javascripts/unit/models/email.js
1
2
3
4
AddressBook.Email = DS.Model.extend({
address: DS.attr('string'),
contact_id: DS.attr('number')
});
Chapter 7
contact_id missing form FIXTURES.
Erratum
spec/javascripts/spec_helper.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Introduction
A Bit About My Motivation
Ive been very interested in the rise of Ember.js and front end web frameworks in general. I recently
made an app for a customer using Angular.js for front-end rendering. There is a very clear focus on
testing in Angular and I found it really easy to build a test suite. Integrating with Rails was much
more of a challenge however. I began to look at Ember and gave it a good go. At the time I found
that persisting data was a real challenge as was integrating with Rails, this was a big disappointment
and in the end I kept going with Angular and was able to produce a well tested app I was happy
with. Angular, however, is much more free form than Ember and presents lots of opportunities for
differences of opinion and approach. Not necessarily a bad thing, but if Im producing an app Id
like another developer to be able to come along and have a very good idea of where to find things
and Ember presents a much higher opportunity to achieve this.
Ive continued to watch the development of Ember and LOVE where its going, the passion and
talent of its developers is amazing and Im so grateful for what they are doing. Most of my initial
difficulties around persistence have been well and truly resolved. Im still, however, frustrated with
the lack of documentation about testing beyond integration tests. There are heaps of blog posts our
there and people have put a lot of work into finding ways to test Ember apps. The problem is, with
all the API changes, many are outdated and simply dont apply any more. That shouldnt be taken
as a criticism of any of the work the authors have put in, Im truly grateful they did.
I had desperately hoped to find a book that would tell me how to do test driven development with
Ember, but it still hasnt eventuated. Ive spent months playing around with different configurations
and setups with Rails and Ember and came to the realisation that in creating my own setup Ive
learned a lot. As a result I thought I would try and write the book I would like to have read!
Im not gonna suggest for a minute that everything Ive put forward is perfect, Im sure its not and
there will definitely be others who will come up with better ideas in the future. Im content that
what Ive put forward is a good starting point. Im very open to feedback and dialogue and want to
create something that helps. If I succeed in helping, please tell others, if not please tell me!
Introduction
A Note on Copyright
If you bought this book or were given a copy by me, skip this!
If you didnt buy this book and found it on a file sharing site, please have a think about it.
This book is a labour of love for me and an attempt to share countless hours of experience
with others. If you feel that your need is so great that you must read without paying,
contact me and I will see what I can do to provide you with a legal copy that receives
regular updates.
Karma is a wonderful thing and if you do choose to take my book without paying, then I
wish you well and I hope that you find it useful and it helps you develop in your career. If
so, please consider buying a copy for someone else who would benefit.
you get pre populated tests for your newly generated model. Personally, I think thats a good thing
for new developers and its easily disabled by seasoned developers.
And so it was that all was good in the world, we rendered on the server, pushed to the client and
everyone was happy. But then
Introduction
As the web has developed and javascript won the war, there became an increasing need to do more
stuff on the client side. We started to see people doing evil things with javascript - anyone remember
the pop-up laden websites of the early 2000s? Not only did javascript facilitate annoying pop-ups
but also helped malicious actions.
Having said all that, some people found really creative uses for javascript and one of the most
successful early ideas was jQuery. jQuery provided developers with a straightforward way to
interact with the Document Object Model (DOM). Developers being lazy (in a good way) found
the $ abstraction very useful.
Javascript DOM selection vs jQuery
1
2
3
4
5
6
7
document.getElementByClassName('container')
document.getElementById('someID')
document.getElementByTagName('p')
// vs
$('.container')
$('#someId')
$('p')
In and of itself, the $ abstraction doesnt do anything to speed up the interface, but does aid developers in providing a one stop shop for interacting with the DOM. Not needing to change methods
speeds development so we dont have to do document.getElementById or document.getElementByTagName,
we can simply adjust the call inside the $ abstraction.
jQuery allowed developers to provide a bit more structure to their javascript, but once an application
grew to a reasonable size, it became a fight to keep the code clean and developers would often
experience call-back hell .
Introduction
I can also see the direction the project is going in and can see huge strides in performance and
convenience with the release of 1.0 . The six week release cycle will see the speed of the framework
improve and minor bugs and problems resolved (though already the benchmarks are impressive in
comparison to other frameworks )
The learning curve for any framework following the convention over configuration pathway is
almost always huge. This is certainly the case with Ember. The initial excitement of being able to
do so much, with so little code soon gives way to the frustration of an error caused by a misnamed
class or incorrect pluralisation.
Although artificial benchmarks are frowned upon, heres some interesting comparisons with backbone.js http://jsfiddle.net/jashkenas/CGSd5/.
Much more impressive is the future with HTMLBars comparing with React.js, Backbone and raw javascript when animating elements. http://jsfiddle.
net/Ut2X6/. HTMLBars is a very exciting potential improvement to Handlebars https://github.com/tildeio/htmlbars
Why Didnt You Use Third Party Libraries to Cover BDD, Such as
Pavlov?
The philosophy Im coming from with the book is that getting as close to the metal as possible will
give you the knowledge and confidence to use third party libraries because you will learn what they
are abstracting away. Knowing the hooks they use and the methods they leverage will allow you to
troubleshoot and make the trade offs you feel are worth it.
You Know That You Can Run Multiple Suites of Tests With
Teaspoon?
Yes, in the first draft of the book I utilised this feature of Teaspoon, but found that there was a
conflict with Guard that led to tests getting stuck in one place, leading to false positive / negative
notifications via Growl. As a result, I chose not to use the feature because I valued reliability higher
than separation of unit and integration tests. The issue with sticking may well get ironed out with
future (or even current) releases of Guard / Teaspoon, so by all means use the feature yourself.
Prerequisites
In order to follow along you will need to have the following installed
Rails 4.1.0
sqlite3
PhantomJS http://phantomjs.org/ - Definitely download a precompiled binary, unless you
want to spend at least 30 minutes compiling form source!
The -T flags tells Rails not to include MiniTest, as well be using RSpec
Next well initialise a git repository.
10
$>
$>
$>
$>
cd address-book
git init
git add .
git commit -m 'initial commit'
source 'https://rubygems.org'
2
3
4
5
6
7
8
9
10
gem
gem
gem
gem
gem
gem
gem
gem
'rails', '4.1.0'
'sqlite3'
'sass-rails', '~> 4.0.0'
'uglifier', '>= 1.3.0'
'coffee-rails', '~> 4.0.0'
'jquery-rails'
'turbolinks'
'jbuilder', '~> 1.2'
11
12
13
14
15
group :doc do
# bundle exec rake doc:rails generates the API under doc/api.
gem 'sdoc', require: false
end
16
17
18
19
group :development do
gem 'spring'
end
Linux users might need a javascript runtime which can be resolved by adding two extra gems execjs and therubyracer
Ember depends on jQuery version 1.7, 1.8, 1.9, 1.10, or 2.0. The jquery-rails gem doesnt always match
these versions, so lets make sure it uses the correct on by setting the gem version. Gem version 3.0.3
will give us jQuery 1.10 .
Computed properties, for example arent valid CoffeeScript syntax, though there are workarounds http://discuss.emberjs.com/t/coffeescriptand-ember/2028 and a project that creates an Ember specific wrapper for CoffeeScript http://emberscript.com/
11
Next, well remove turbo link and jquery-ujs from the asset pipeline
app/assets/javascripts/application.js
1
2
3
4
//=
//=
//=
//=
require jquery
require jquery_ujs
require turbolinks
require_tree .
<head>
<title>AddressBook</title>
<%= stylesheet_link_tag
"application", media: "all", "data-turbolinks-track"\
=> true %>
<%= stylesheet_link_tag
"application", media: "all"%>
<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= javascript_include_tag "application"%>
<%= csrf_meta_tags %>
</head>
Now well remove the old gems using bundler. Note the --local flag, this tells bundler to only look
at things already installed locally and avoids hitting rubygems. This can be a great speedup, but
obviously doesnt help if your dont have a particular gem already available locally.
Running bundler to remove unnecessary gems
1
12
Committing changes
1
2
group
gem
gem
gem
gem
gem
end
:development do
'spring'
'quiet_assets'
'meta_request'
'better_errors'
'binding_of_caller'
Then:
See this great screencast by Ryan Bates on Better Errors and RailsPanel http://railscasts.com/episodes/402-better-errors-railspanel
13
Install Ember
Lets get on now and install Ember. The core team have very kindly provided a great gem to help us
integrate Ember with rails.
Lets add it to our Gemfile.
Adding Ember to our Gemfile
1
2
gem 'ember-rails'
gem 'ember-source', '1.5.0'
Ember-rails provides us with a lot of development dependencies such as the Ember.js files in
development and production versions, a precompilation pathway for Handlebars templates and
ActiveModel::Serializer, which well use heavily for passing data back and forth between the server.
After we run bundler, we can use the build in generator to get Embers prerequisites installed.
Bootstrapping Ember
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
create
create
create
create
create
create
18
19
20
21
22
23
14
app/assets/javascripts/templates/components/.gitkeep
app/assets/javascripts/mixins
app/assets/javascripts/mixins/.gitkeep
app/assets/javascripts/address_book.js
app/assets/javascripts/router.js
app/assets/javascripts/store.js
Whoah! Thats a whole bunch of setup thats been done for us! What youll see from this is the
influence of convention over configuration. Theres a place for everything and everything should
be in its place! At this stage, most of these files are only for place holding (.gitkeep tells git keep the
folder in the source control even if it is empty), except of the .js files. Well see what they are about
in depth later.
Views vs Templates
What you know as a view in Rails is almost certainly a template in Ember! Its pretty easy
to get caught out with these distinctions and Im sure youll find yourself putting files in
the wrong place as you learn. If a page isnt rendering as you expected, ask yourself if the
file is the correct directory.
Next well ensure that we have the necessary Ember javascripts available.
Ember javascript dowload
1
This will go off and download the development and production javascripts necessary for Ember.
Now that weve got Ember available there are a few files that need updated to get us to a good base
point.
We can now delete the app/view/layouts folder and its contents (application.html.erb).
.
Next, update your routes
15
config/routes.rb
1
2
3
4
Rails.application.routes.draw do
root to: 'assets#index'
get 'assets/index'
end
We will also need a Rails view to render, so go ahead and create one.
app/views/assets/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title>Address book</title>
<%= stylesheet_link_tag
"application", :media => "all" %>
<%= csrf_meta_tags %>
</head>
<body>
<%= javascript_include_tag "application" %>
</body>
</html>
When using ember-rails youre default starting point for rendering is a handlebars file called
application.hbs. This file will be rendered as your root route. Lets create it.
app/assets/javascripts/templates/application.hbs
1
2
3
{{outlet}}
Ok, let check that all is well, fire up a server (rails s) and see for yourself. If all is well in the world,
you should see:
If not, its time for some debugging! See you when you get back.
16
.
For those not familiar with PhantomJS, its a headless browser based on Webkit. It allows us a very
nice way to run tests without having to open a browser and constantly refresh the page.
RSpec
Most rails developers will be familiar with RSpec and the opinions of DHH, I tend to disagree and
find that RSpec provides a great way of thinking about my apps and as a result the tests flow from
my brain to the text editor very simply.
Getting started with RSpec is very straightforward and we will also turn off automatic spec
generation to avoid unnecessary kruft! Well also use FactoryGirl to for generating fixtures during
tests.
For more information on DHHs opinion http://www.rubyinside.com/dhh-offended-by-rspec-debate-4610.html
18
group
gem
gem
gem
end
:test, :development do
'factory_girl_rails'
'rspec-rails'
'spring-commands-rspec'
From Rails 4.1, spring is included by default, which is an amazing speed booster because it preloads
your enviroment for you, significantly reducing the time taken to run migrations, generators and
spec. By default it doesnt support RSpec, but the spring-commands-rspec gem sorts that our for us.
Well now stop Rails from auto generating view, helper, controller and routing specs. Whilst the
built in routing and controller specs can be useful, Ive found that they dont work so well with
namespaced routes and controllers which we will use later. As a result I found that correcting them
took longer than writing from scratch. We will now update the application initialiser to prevent it
from auto generating some specs when we use rails generators.
config/application.rb
1
2
3
4
5
6
7
8
9
module AddressBook
class Application < Rails::Application
config.generators do |g|
g.test_framework :rspec, fixtures: true, view_specs: false,
helper_specs: false, controller_specs: false, routing_specs: false
g.factory_girl true
end
end
end
bundle install
rails g rspec:install
19
Committing changes
1
2
git add . -A
git commit -m 'Adding in RSpec'
Teaspoon
Teaspoon provides both a beautiful an elegant way interface for javascript testing on Rails, theres
also a plugin for Guard which means we can make automated testing very straightforward.
I particularly like keeping my unit tests separate from my integration tests. Teaspoon has the notation
of suites allowing us to make simple switches. We can use fixtures or interact with live data from
our server.
Another one of the niceties of Teaspoon is that QUnit, Jasmine and Mocha are shipped by default,
so no extra dependencies to manage.
So, lets get started with Teaspoon, by adding it to our Gemfile inside the :test, :development
group. Well also take the opportunity to add in Guard and optionally Growl.
Gemfile
1
2
3
4
5
6
7
8
9
10
We add in the support for spring commands with Teaspoon also to help with speeding up tests.
After running bundler well initialise Teaspoon.
and still get continuous feedback about how my tests are going. Its only available for Mac though. http://growl.info/
20
Initializing Teaspoon
1
2
3
4
5
6
7
8
//......
Teaspoon.configure do |config|
config.suite do |suite|
suite.use_framework :qunit
suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee\
}"
suite.helper = "spec_helper"
suite.stylesheets = ["teaspoon"]
end
end
One issue that crops up when using Teaspoon with Rails 4.1 is that asset pipeline issues are set to
raise runtime errors in development mode. This causes issues with Teaspoon and can be resolved by
adjusting turning off the runtime error mode.
config/initializers/development.rb
1
2
3
4
Rails.application.configure do
//.....
config.assets.raise_runtime_errors = false
...
Now lets convert our application.js to application.js.erb to give us a nice place to turn on
and off Ember debugging flags.
21
app/assets/javascripts/application.js.erb
1
2
3
4
5
6
//=
//=
//=
//=
//=
//=
require jquery
require handlebars
require ember
require ember-data
require_self
require address_book
7
8
9
10
11
12
13
14
AddressBook = Ember.Application.create();
15
16
//= require_tree .
We can now make changes in the development block that wont affect our site in production.
Next well add adjust the spec_helper.js to setup Ember for testing. Now theres a fair bit going
on here. First we require the main application.js.erb then we add two divs to allow a place for
our app to run.
Next we tell our app to run in the ember-testing div and use the setupForTesting() helper.
This stops the Ember app from going through the run loops, except when we use the wrapper
Ember.run(function() { some stuff to be done}) . We also injectTestHelpers() which gives
us access to a whole bunch of useful functions that we can use for testing.
New file - spec/javascripts/spec_helper.js
1
2
3
4
5
var d = document;
d.write('<div id="ember-testing-container"><div id="ember-testing"></div></div>');
6
7
8
9
AddressBook.rootElement = "#ember-testing";
AddressBook.setupForTesting();
AddressBook.injectTestHelpers();
More information on the run look and TDD here http://instructure.github.io/blog/2014/01/24/ember-run-loop-and-tdd/
22
In order to make our tests look a bit nicer when using web view well add a splash of CSS. The styling
comes from the Discourse project, one of the largest open source Ember apps in development.
New file - app/assets/stylesheets/teaspoon_custom.css
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ember-testing-container {
position: absolute;
background: white;
width: 640px;
height: 384px;
overflow: auto;
z-index: 9999;
border: 1px
solid #ccc;
right: 50px;
top: 200px;
padding: 5px;
}
#ember-testing { zoom: 50%; }
And now we will initalise the binstubs so that spring can help us out with running our tests quickly.
Generate binstubs
1
Theres a fair bit of magic going on behind the scenes with this some of which is useful to know. The
first time you run rails after the binstubs are generated, spring fires up you app. Youll see a deloy
until the applicaiton is ready. Once spring is up, future commands will run more quickly. Spring
intelligently reloads any altered files in future, so youll find that any command run from here on
will get started much more quickly.
Guard
Before moving of to writing some actual tests (I know, its been a long time coming!), well install
Guard so we can get continuous feedback.
Because we added it to the bundle earlier, getting it running is really simple.
Discourse is pitched as a next generation discussion platform, aimed at making forums nice! Ive used it extensively and its just beautiful to use
and Ive learned massively by making small contributions to the development. Learn more here https://github.com/discourse/discourse/
23
Initalize Guard
1
Well update the Guardfile that gets generated to take advantage of what spring has to offer.
Guardfile
1
2
3
guard
Assuming all is well you should see a Guard running RSpec and the three suites defined with
Teaspoon. There should be 0 examples and 0 failures at this stage. If not, its time to backtrack
and find out where the issue is.
If all was well, lets commit our changes now.
Commit changes
1
2
git add . -A
git commit -m 'Teaspoon and Guard'
AddressBook.Router.map(function() { });
At the moment this doesnt seem very impressive, but we will soon discover we actually got quite
a lot from that small declaration.
Lets go ahead and see what we can discover about the router by writing a spec.
module('Routing specs', {
setup: function () {
AddressBook.reset();
}
});
Stop breaking the web http://2013.jsconf.eu/speakers/tom-dale-stop-breaking-the-web.html
25
At the moment, this is mainly a placeholder, but the reset() call on our app in the setup is an
important step which will be called before each test is run - it ensures that our app is returned to
its initial state. I prefer to put this in the setup, rather than teardown as it makes for easier visual
debugging when running test in the browser as it leaves the output on screen. When you focus on
a single failing test, this presents the exact look of the failure for you.
Tests are then declared using a test declaration which taken a string as its name and an anonymous
function containing the actual test.
spec/javascripts/unit/routing/router_spec.js
1
2
3
4
5
6
7
8
If you run this spec now, youll see we have a failing test!
Spec output
1
Failures:
2
3
4
Lets commit those changes and then well dig into what weve just discovered.
Commit changes
1
2
git add .
git commit -m 'First failing spec'
26
Parameters
Explanation
visit(url)
URL as string
andThen(func)
Anonymous function
equal(thingUnderTest,
expectedResult, optional
error message)
thingUnderTest=Object, string,
anything really!, expectedResult=
speaks for itself, optional error
message can be a string, a
variable or a combination
So what weve done is visited the root route for our app and when the promise has been returned
weve started digging about in Embers innards!
We set a variable current_route for use in our test. Whilst setting a variable isnt strictly necessary
it gives us a nice construct for reporting on the outcome of the test.
Breaking it down
AddressBook = the name of our application
__container__ = an Ember namespace construct. NEVER use this in production, only use for
.
Now in our failing test we see that currentRoute is index. This top level route is created for us
automatically by Ember. We now know how to make our test pass and can update it to provide a
more useful error message.
Martin Fowler explains promises very simply here http://martinfowler.com/bliki/JavascriptPromise.html. Chris Webb provides a much more
detailed explanation here http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt1-theory-and-semantics
27
spec/javascripts/unit/routing/router_spec.js
1
2
3
4
5
6
7
8
Now clearly this wasnt exactly a test first approach, but with this bit of exploration we know how
to lookup URLs and see how they are resolved.
Commiting changes
1
2
git add .
git commit -m 'Routing spec to green'
9
10
});
28
Routing failure
1
Failures:
2
3
4
5
6
7
8
Great, we now have failing spec and we now know that we need to define a contacts route.
app/assets/javascripts/router.js
1
2
3
AddressBook.Router.map(function() {
this.resource('contacts');
});
And with that we should now have a passing spec! That was very straightforward and is a testament
to the convention over configuration approach.
Commiting changes
1
2
git add .
git commit -m 'Create a contacts route in Ember'
29
module('Contacts integration', {
setup: function () {},
teardown: function () {
AddressBook.reset();
}
});
7
8
9
10
11
12
13
14
15
Were introduced to a new helper here find(). The find helper takes a string which explains what
were looking for. Its usage is essentially the same as the jQuery find() API and allows us to
create complex lookups by nesting searches. For now though were simply going to look for a class
called contacts_heading.
app/assets/javascripts/templates/contacts/index.hbs
1
So we should have a passing test now, right? Sadly not. So, whats wrong? Well it turns out that when
we specified the route earlier, what we got was a route that looks for contacts.hbs. Now it wouldnt
be very sensible to keep all of our templates in one directory, so I created it in the contacts directory
and called it index.hbs. With the pre-compilation process it exists in Ember as contacts.index. We
dont have a route to contacts.index though, so how to create it?
Well I tricked you earlier with an incorrect use of the this.resource, I know, naughty hey! Bear
with me though and youll see why.
In order to fix this, we need to adjust the router.
30
app/assets/javascripts/router.js
1
2
3
4
AddressBook.Router.map(function () {
this.resource('contacts', function () {
});
});
We pass in an anonymous function and our integration spec goes green. Uh Oh, now our router spec
is red!
Router spec error
1
Failures:
2
3
4
Perfect, that error tells us exactly what went wrong and shows the value of taking the time to specify
verbose errors. So we have now learned that this.resource('contacts') creates a simple route
called contacts, but when passed an anonymous function the route disappears and is replaced by
contacts.index. Essentially whats happened here is that Ember has assumed that when we passed
the anonymous function we plan to create a bunch of other nested routes. Clever Ember!
Lets update our spec to reflect reality now.
spec/javascripts/unit/routing/router_spec.js
1
OK great, weve now got a some routing specs and an integration spec, lets commit our changes.
Commiting changes
1
2
git add .
git commit -m 'First integration spec'
31
spec/javascripts/support/testing_helpers.js
1
2
3
4
5
6
7
8
9
This provides us with a really terse way of testing routes, tell it were we want to go and what route
we expect to see. Lets add it to the spec_helper.js
spec/javascripts/spec_helper.js
1
4
5
6
7
Lovely, doesnt that nice clean tidy code make you feel all warm inside??
Lets commit our changes and then well move on to putting some actual data on the page.
Commit changes
1
2
git add .
git commit -m ''
Great, now weve got a failing spec, lets get on and get things working.
The first thing we will need in order to get this spec passing is a Route. Didnt we already create
a route I hear you ask? Well yes we did, but that was for routing not a route! Basically, when
you declare a route in the Router an Ember.Route is created for you automatically. The Route for
contacts.index is called ContactsIndexRoute, this is great if we dont want Route to do anything,
but if we want to create some functionality we will need to customise it.
New file - app/assets/javascripts/routes/contacts/contacts_index_route.js
1
2
3
4
5
AddressBook.ContactsIndexRoute = Ember.Route.extend({
model: function () {
return this.store.findAll('contact'); // Note that contact is singular
}
});
What weve done here is said that when we visit the contacts index, go to the application data store
and find all records for the contact data model and attach the result to a model property.
33
If we try to run our spec now well get the following error somewhere in the output. Like Rails,
when running specs the actual problem we need to fix can appear in the stack trace, rather than the
actual spec output.
Error - truncated
1
2
3
4
5
Error while loading route: Error: No model was found for 'contact'
at http://127.0.0.1:57169/assets/ember-data.js?body=1:3027
at http://127.0.0.1:57169/assets/ember-data.js?body=1:2629
at http://127.0.0.1:57169/assets/routes/contacts/contacts_index_route.js?body\
=1:3
Models
So we now know that there is no contact model available, so lets declare a model for storing our
contact information.
New file - app/assets/javascripts/models/contacts.js
1
2
3
4
AddressBook.Contact = DS.Model.extend({
first_name: DS.attr('string'),
last_name: DS.attr('string')
});
We can, if we choose simply declare DS.attr without specifying the type but, my preference is to
be explicit about what we are expecting.
Running our spec again we will see that we are getting Error while loading route: undefined.
That seems odd because we know that we have defined the route, so whats going on? When we
visit a route and ask for the model, Ember automatically makes a request to the server for the data.
In our case its going to hit http://localhost:3000/contacts. Anyone familiar with Rails will
quickly recognise that we havent created any of the Rails infrastructure necessary to support this
call so it responds with a 404 (not found) error. This failure results model call in the route to return
undefined and stalls the application.
Its not very useful to have the test suite hitting the server for data all the time anyway, so lets
ensure that we can get some data internally.
34
spec/javascripts/integration/contacts_integration_spec.js
1
2
3
4
5
6
7
8
module('Contacts integration', {
setup: function () {
AddressBook.ApplicationAdapter = DS.FixtureAdapter;
},
teardown: function () {
AddressBook.reset();
}
});
The Ember Fixture Adapter allows us to have pre specified data to use in our tests. This makes it
very easy for us to know what to test for.
Fixture error
1
2
3
4
5
Error while loading route: Error: Assertion Failed: Unable to find fixtures for m\
odel type AddressBook.Contact
at http://127.0.0.1:57169/assets/ember.js?body=1:81
at http://127.0.0.1:57169/assets/ember-data.js?body=1:8109
at _findAll (http://127.0.0.1:57169/assets/ember-data.js?body=1:3590)
Awesome, we are now told that there are no fixtures available for our Contact. Fixtures are specified
as a javascript Array of Objects.
spec/javascripts/integration/contacts_integration_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module('Contacts integration', {
setup: function () {
AddressBook.ApplicationAdapter = DS.FixtureAdapter;
AddressBook.Contact.FIXTURES = [
{
id: 1,
first_name: 'Dave',
last_name: 'Crack'
},
{
id: 2,
first_name: 'Dustin',
last_name: 'Hoffman'
}
]
35
},
teardown: function () {
AddressBook.reset();
}
});
Great, weve finally got a useful error, and if youre wondering about Dustin Hoffman and Dave
Crack, see the footnote!
Error message
1
2
We can now update our index template to render the data weve just setup.
app/assets/javascripts/templates/contacts/index.hbs
1
2
3
4
5
6
Here we create an unordered list and using the {{#each}}{{/each}} helper, asking Ember to create
a <li> element for each item in the array. Inside the iterator we ask for the first_name property to
be rendered. The first name is received from the model
With that we should now have a passing spec.
Commit time again!
Commit changes
1
2
git add .
git commit -m 'Contacts integration spec to green'
36
RSpec example
1
2
3
subject { Contact.new }
it {should respond_to :some_property_name }
it {should respond_to :some_other_property }
This pattern gives me confidence that my model has the attributes I need to handle data. We can do
something similar with Ember.
New file - spec/javascripts/unit/models/contacts_spec.js
1
2
3
4
5
6
module('Contacts Model', {
setup: function () {},
teardown: function () {
AddressBook.reset();
}
});
7
8
9
10
11
12
When we create a Model in Ember it is attached to the top level namespace, hence AddressBook.Contact.
With the metaForProperty('first_name') call we receive an object with metadata for first_name:
DS.attr('string') The subsequent calls check that it is an attribute and that the type is string.
I like to use this pattern frequently enough to create a helper which requires a spot of light
metaprogramming! Well also introduce the ok() test helper. ok() takes an argument that should
evaluate as a boolean and an optional second argument that is the error message.
spec/javascripts/support/testing_helpers.js
1
2
3
4
5
6
We can then refactor our original to use the new helper and check the other property.
37
spec/javascripts/unit/models/contacts_spec.js
1
2
3
4
test('attributes', function () {
respondsTo('Contact', 'first_name', 'string');
respondsTo('Contact', 'last_name', 'string');
});
Now there are many argument for and against this pattern in testing. Many would argue that its
brittle and unnecessary. I can definitely see this perspective, particularly for a seasoned developer,
but I believe that whilst learning the workings of the framework declaring what were looking
for before we implement it gives us great insight into whats going on internally. Later when we
encounter the errors in a less controlled manner we can have confidence that we know what to do.
As your confidence grows you can rely less on this kind of pattern.
Obviously we didnt test the Model first in this chapter, but we will in future.
Well commit our changes now and in the next chapter well look at nested routes.
Committing changes
1
2
git add .
git commit -m 'Model testing and helper'
An Unexpected Problem
In defining the ContactsIndexRoute weve unwittingly introduced a problem into the test suite.
The problem is that if we run the routing spec on its own we will now get a failure reporting
Failure/Error: Expected contacts.index, got: index. The reason for this is that now we
have declared the ContactsIndexRoute the first thing we ask it to do is to retrieve all contacts
from the server. We dont have anything setup in Rails at this stage so the server returns a
404 - Not Found error, which means we cant transition to contacts.index. This happens if we
try to move from / to /contacts. If we try to hit /contacts without first hitting / we get
a different error Failure/Error: TypeError: 'undefined' is not an object (evaluating
'AddressBook.__container__.lookup('controller:application').currentRouteName').
Both of these problems can be resolved by moving the FixtureAdapter to our initial test helper so
that the real server is never hit during testing.
Lets do that now.
spec/javascripts/spec_helper.js
1
2
3
4
5
6
7
8
9
10
11
12
13
AddressBook.ApplicationAdapter = DS.FixtureAdapter;
AddressBook.Contact.FIXTURES = [
{
id: 1,
first_name: 'Dave',
last_name: 'Crack'
},
{
id: 2,
first_name: 'Dustin',
last_name: 'Hoffman'
}
];
git add .
git commit -m 'Refactor FixtureAdapter'
38
What we want
contacts
contacts/:contact_id
contacts/new
With this in mind we can now define our nested routes and I will use the Rails style of index, show,
new etc.
Lets start with a failing spec.
spec/javascripts/unit/routing/router_spec.js
1
2
3
4
//....
test('individual contact', function () {
routesTo('/contacts/1', 'contacts.show');
});
4
5
6
7
40
app/assets/javascripts/router.js
1
2
3
4
5
AddressBook.Router.map(function () {
this.resource('contacts', function () {
this.route('show', {path: '/:contact_id'});
});
});
Our spec should now be green. Lets consider what weve just done in a bit more detail.
Use case
Notes
this.resource('name',
function () {})
this.route('names', {path:
'/:segment_id'})
In our example we get a route called contacts.show which takes the final URL segment as a
parameter which gets bound to contact_id which we can use in our controller for looking up the
record.
We will now create a route for an individual record and an associated template.
New file - /app/assets/javascripts/routes/contacts/contacts_show_route.js
1
2
3
4
5
AddressBook.ContactsShowRoute = Ember.Route.extend({
model: function (params) {
return this.store.find('contact', params.contact_id);
}
});
The main difference here between our ContactsShowRoute and the the ContactsIndexRoute is that
the model only asks for one contact and does so via the params.contact_id. contact_id got defined
in our router via {path: '/:contact_id'}.
41
//....
test('Renders only one contact', function () {
visit('/contacts/1');
andThen(function () {
var contact = find('#contact h1').text();
var expected_result = 'Details for Contact 1';
equal(contact, expected_result, 'Expected: ' + expected_result + ' got: '\
+ contact);
});
});
With this test we will visit the newly defined route and the use the find helper to find an html
element with the id of contact and collect the text. We expect that this text is Details for Contact
1.
We can now create the template.
New file - app/assets/javascripts/templates/contacts/show.hbs
1
2
3
4
5
6
7
<div id="contact">
<h1>Details for Contact {{id}}</h1>
<ul>
<li>First Name: {{first_name}}</li>
<li>Last Name: {{last_name}}</li>
</ul>
</div>
Here we are using the id , first_name and last_name attributes in our template.
And with that our tests suite should be green.
Refactor Tests
Lets remove the call to AddressBook.reset() from all of our tests (router_spec.js , contacts_spec.js , contacts_integration_spec.js) and keep it in the spec_helper.js now. This removes
duplication and ensures that we dont forget to put it in anywhere.
42
spec/javascripts/spec_helper.js
1
2
AddressBook.injectTestHelpers();
# ....
3
4
5
6
QUnit.testStart = function () {
AddressBook.reset();
};
The change of reseting the app before each test rather than afterwards gives us an opportunity to
have a look at the application state in the browser. Lets see how that is done.
This is kinda neat! The CSS we set up earlier is a trick that allows us to render the results of the tests
in the bottom right hand corner of the screen. These are not screenshots, but our actual application.
You can go ahead and click around in there are navigate your app in the test environment.
43
This is particularly useful because we will see the results of a failing spec here and can really visualise
whats going on. I prefer to rely mostly on the command line approach and move to the web interface
if I just cant figure out why things are going wrong.
//..
test('Visiting a contact via the index screen', function () {
visit('/contacts').click('ul li:last a');
andThen(function () {
var contact = find('#contact h1').text();
var expected_result = 'Details for Contact 2';
equal(contact, expected_result, 'Expected: ' + expected_result + ' got: '\
+ contact);
});
});
Here we learn some other new tricks! The visit() helper can be chained, so we visit the index route,
the we chain the click() helper. By nesting our search we can ensure that we click the last link in
the list and not the first.
find ul -> then find last li element -> then find the a tag -> then click it!
Once weve clicked the link we would expect to be on the details page for contact 2 this time.
I like to change up the details I check (i.e. contact 2 this time, not 1) for in this manner as it helps to
detect any issues I may have not picked up otherwise.
With that, we should have a failing spec similar to this
44
Failure
1
2
<ul class="contacts_list">
{{#each}}
<li>{{#link-to 'contacts.show' this}}{{first_name}}{{/link-to}}</li>
{{/each}}
</ul>
With this helper we are asking Ember to go to the contact.show route which is passed as a string
and to use this for the parameter. Each time the helper is iterated over this represents the record /
object being rendered. Ember is able to use the record id to build the correct URL.
And with that, were back to green.
Time for a commit before we switch things over to Rails to get an idea about the infrastructure
needed to allow persistence in our app.
Commiting changes
1
2
git add .
git commit -m 'Individual contacts and specs'
invoke
create
create
invoke
create
invoke
create
active_record
db/migrate/20140204004246_create_contacts.rb
app/models/contact.rb
rspec
spec/models/contact_spec.rb
factory_girl
spec/factories/contacts.rb
Yehuda discussed in detail the problems in developing Ember Data on the Ember Hot Seat podcast (From about 41 minutes onwards). http:
//emberhotseat.com/2013/07/19/ember-hot-seat-episode-007.html
46
spec/models/contact_spec.rb
1
require 'spec_helper'
2
3
4
describe Contact do
let(:contact) { FactoryGirl.build_stubbed(:contact) }
5
6
subject { contact }
7
8
9
10
In this test we use the #build_stubbed method. I particularly like this pattern because it generates
an instance of the model, but without any persistence. This is great because it keeps our tests really
fast because the database doesnt get hit. We can already feel confident that the persistence layer of
Rails is very well tested, so theres no need to double up.
Were testing here that our model has first_name and last_name properties. If we run the spec now
well get a failure because we havent migrated the database.
Lets fix that.
Migrate
1
Well need a controller and a route to pass our data back and forth.
We will use a namespaced controller which will allow us to create our API in a partitioned way.
This, in my opinion, is good for our ability to swap things out in the future.
New file - spec/controllers/api/v1/contacts_controller_spec.rb
1
require 'spec_helper'
2
3
describe Api::V1::ContactsController do
4
5
end
And with that we should have a failing spec that looks similar to this.
47
2
3
end
# ...
2
3
4
5
6
7
8
9
10
describe Api::V1::ContactsController do
describe 'GET methods' do
it 'index' do
@contacts = FactoryGirl.create_list(:contact, 2)
get :index
end
end
end
We ask FactoryGirl to create (and persist) two contacts so we have some data to work with and to
try to GET the index method on our controller. Well get a failing spec similar to this
Failing spec
1
2
3
4
48
config/routes.rb
1
# ......
2
3
4
5
6
7
8
get 'assets/index'
namespace :api do
namespace :v1 do
resources :contacts
end
end
This is one of my personal frustrations with controller testing in Rails - the errors you get when a
view isnt defined. There are two ways we can fix this, we can create views/api/v1/index.html.erb
as an empty file or we can update our controller to render nothing. I prefer the second approach
because we are not actually going to be creating standard views.
49
/app/controllers/api/v1/contacts_controller.rb
1
2
3
def index
render json: nil
end
Our specs should now be passing. Simply rendering nothing isnt particularly useful though, so next
lets expect some data.
spec/controllers/api/v1/contacts_controller_spec.rb
1
2
get :index
assigns(:contacts).length.should == 2
def index
@contacts = [1,2]
render json: @contacts
end
Great, our spec is green again. Next we should get it passing some real data.
spec/controllers/api/v1/contacts_controller_spec.rb
1
2
3
get :index
assigns(:contacts).length.should == 2
assigns(:contacts)[0].class.should == Contact
Now we want to make sure that the first element in our @contacts is an instance of the Contact
class.
50
/app/controllers/api/v1/contacts_controller.rb
1
2
3
4
def index
@contacts = Contact.all
render json: @contacts
end
git add .
git commit -m 'Build Rails API for Contacts'
We define a serialiser which intercepts our @contacts from the controller and adjusts the output
format before passing it back to the render json: call.
The serialiser we just created trims the output to only include the id, first_name and last_name.
Timestamps are excluded.
Below are before and after examples of the differences in JSON formatting just so you can get your
mind around it.
51
{"contacts":[{"contacts":{"id":1,"first_name":"Dave","last_name":"Crack","created\
_at":"2014-02-04T02:16:38.491Z","updated_at":"2014-02-04T02:16:38.491Z"}},{"conta\
cts":{"id":2,"first_name":"Dustin","last_name":"Hoffman","created_at":"2014-02-04\
T02:16:54.229Z","updated_at":"2014-02-04T02:16:54.229Z"}}]}
After creation
1
2
{"contacts":[{"id":1,"first_name":"Dave","last_name":"Crack"},{"id":2,"first_name\
":"Dustin","last_name":"Hoffman"}]}
Apart from the obvious removal of the timestamps, you can also see that the root contacts is gone.
All of the contacts are now part of one array and has a clean and legible structure.
We can also create a big of sample data to show in our app.
Rails console
1
rails console
52
/app/assets/javascripts/store.js
1
2
3
4
5
6
AddressBook.Store = DS.Store.extend({
adapter: '-active-model'
});
DS.RESTAdapter.reopen(
{namespace: "api/v1"}
);
Here were telling Ember to use the DS.ActiveModelSerializer for our data. We also tell the
DS.RESTAdapter that all of our JSON requests should be prefixed with api/v1. In this way should
we ever wish to change our API we simply updated to a new version.
You will now be able to move around your app and navigate between contacts.
Time to commit again
Committing changes
1
2
git add .
git commit -m 'Connecting the frontend to the backend'
53
/app/controllers/api/v1/contacts_controller.rb
1
2
3
4
5
#..
def show
@contact = Contact.find(params[:id])
render json: @contact
end
Specs to green, but lets refactor our controller and take advantage of StrongParameters.
/app/controllers/api/v1/contacts_controller.rb
1
2
3
4
5
6
7
8
9
def show
render json: @contact
end
10
11
private
12
13
14
15
def set_contact
@contact = Contact.find(params[:id])
end
16
17
end
Awesome, thats nice and neat and in keeping with the patterns promoted by StrongParameters.
Commit changes
1
2
git add .
git commit -m 'Show single contact'
For more information on StrongParameters see https://github.com/rails/strong_parameters and http://guides.rubyonrails.org/action_controller_
overview.html#strong-parameters
54
Adding Contacts
So far weve only dealt with data weve pushed directly into the store, we will now look at how to
add a contact of our own.
A reasonable pattern for this to have a button for adding which changes the view to present us with
an input field, a save button and a cancel button.
Lets start with an integration spec for showing an input field.
spec/javascripts/integration/contacts_integration_spec.js
1
2
3
4
5
6
7
8
//..
test('Show input for new contact', function () {
visit('/contacts').click('#add_new_contact');
andThen(function () {
var input_field = find('#new_first_name').length;
ok(input_field == 1, 'Input field not found');
});
});
So we expect that when we visit /contacts and click the button with id add_new_contact then we
will see an input field with an id of new_first_name.
/app/assets/javascripts/templates/contacts/index.hbs
1
2
3
Failures:
2
3
4
So how do we go about fixing this up? Firstly we will need to create a controller for our contacts
route which will give us a place to hang actions from.
55
AddressBook.ContactsIndexController = Ember.ArrayController.extend({
actions: {
addNewContact: function () {
6
7
});
We will add the action to the template now and then define the action required to get the spec
passing.
/app/assets/javascripts/templates/contacts/index.hbs:1
1
2
3
Our spec at this point will still be failing because although we have a button, clicking it doesnt do
anything just yet.
We should define an action to occur on click now.
/app/assets/javascripts/controllers/contacts_index_controller.js
1
2
3
4
actions: {
addNewContact: function () {
this.toggleProperty('addingNewContact');
}
We will now use a conditional in our template to show the input field based on whether
addingNewContact is true or not.
56
/app/assets/javascripts/templates/contacts/index.hbs
1
2
3
4
5
6
7
{{#if addingNewContact}}
<label for="new_first_name">First name</label>
{{input type='text' value=controller.new_first_name id='new_first_name'}}
{{else}}
<button id="add_new_contact" {{action 'addNewContact'}} >Add new contact</but\
ton>
{{/if}}
With this update we show the button if we are not editing and if we are, then we hide the button and
show a the new_first_name input field. We use the ember input helper to create a text input field with
the correct id. We also use value to bind the input to a controller property called new_first_name.
This does not need to be declared ahead of time, Ember just takes care of it for us.
And with that our spec should be green.
Well add a bit more now, because most people have a first and a last name! With that in mind, lets
update the spec we wrote.
spec/javascripts/integration/contacts_integration_spec.js
1
2
3
4
5
6
7
//.....
andThen(function () {
var first_name_field = find('#new_first_name').length;
var last_name_field = find('#new_last_name').length;
ok(first_name_field == 1, 'First name field not found');
ok(last_name_field == 1, 'Last name field not found');
});
{{#if addingNewContact}}
<label for="new_first_name">First name</label>
{{input type='text' value=controller.new_first_name id='new_first_name'}}
<label for="new_last_name">Last name</label>
{{input type='text' value=controller.new_last_name id='new_last_name'}}
spec/javascripts/integration/contacts_integration_spec.js
1
2
3
4
5
6
7
8
9
10
11
//.....
test('Adding a new contact', function () {
visit('/contacts').click('#add_new_contact');
fillIn('#new_first_name', 'Buzz');
fillIn('#new_last_name', 'Lightyear');
click('#save_new_contact');
andThen(function () {
var first_name = find('.contacts_list:contains("Buzz")').length;
ok(first_name == 1, "First name was not saved");
});
});
Lets get started with updating the template by adding the save button.
/app/assets/javascripts/templates/contacts/index.hbs
1
2
3
4
5
{{#if addingNewContact}}
//....
{{input type='text' value=controller.new_last_name id='new_last_name'}}
<button id="save_new_contact" {{action 'saveNewContact'}} >Save new contact</butt\
on>
Failures:
2
3
4
5
6
57
58
/app/assets/javascripts/controllers/contacts_index_controller.js
1
2
3
4
5
6
actions: {
//...
},
saveNewContact: function () {
var new_first_name = this.get('new_first_name');
var new_last_name = this.get('new_last_name');
8
9
10
11
12
new_contact.save();
13
14
Again, our spec is green, but it would be great if the text fields were cleared and we were returned
to the initial view with an add button.
spec/javascripts/integration/contacts_integration_spec.js
1
//....
ok(first_name == 1, "First name was not saved");
var add_new_contact_button = find('#add_new_contact').length;
ok(add_new_contact_button == 1, "Have not transitioned back to original s\
2
3
4
5
tate");
This new expectation test to ensure that we can see the add new contact button again.
When we call the save() function Ember returns a promise. We can respond to that with .then()
which takes two optional arguments, the first to handle success and the second to handle failure.
Lets use that to go back to the original view if we succeed and to show an alert if it fails.
59
/app/assets/javascripts/controllers/contacts_index_controller.js
1
2
3
4
5
6
7
8
9
10
11
saveNewContact: function () {
//..
var self = this;
new_contact.save().then(
function ( {
self.set('new_first_name', '');
self.set('new_last_name', '');
self.toggleProperty('addingNewContact');
},
function () {alert('Unable to save record'); });
}
With the promise pattern its important to know that when the promise returns, this may
no longer be this, it may be another this! In order to prevent any issues we explicitly
set a reference to the current context by var self = this;. In the success portion of our
promise, we call the .toggleProperty on self.
The two this.set calls, these arent strictly necessary, but I think its nice if when a user later clicks
add that they start with a blank slate :)
Spec is now green, which is great, but if we want to actually save the data in our backend, well
need to create a create method on our controller in Rails.
As always, we will starting with a failing spec!
spec/controllers/api/v1/contacts_controller_spec.rb
1
2
3
4
5
6
7
8
describe Api::V1::ContactsController do
#......
describe 'POST methods' do
it 'creates a new contact' do
@contact = FactoryGirl.attributes_for(:contact)
expect{post :create, contact: @contact}.to change(Contact, :count).by(1)
end
end
Here we use the FactoryGirl method #attributes_for to get a hash of attributes. When we call post
:create we are sending a HTTP POST request and with contact: @contact we are sending the
attributes as JSON. We wrap the call in an expect block and tell it we that Contact.count should
change by 1.
60
/app/controllers/api/v1/contacts_controller.rb
1
2
3
4
5
6
7
8
9
#.....
def create
@contact = Contact.new(get_contact_params)
if @contact.save
render json: @contact
else
render json: @contact.errors, status: :unprocessable_entity
end
end
10
11
12
13
14
15
private
#....
def get_contact_params
params.require(:contact).permit([:first_name, :last_name])
end
For those not familiar with the StrongParameters pattern, in get_contact_params we state that we
require the POST request to have a contact, and that on that contact we will permit a first_name
and a last_name. Anything else will be discarded. This pattern helps us to keep malicious code out
of our system. Perhaps, for example a user model has an admin property, if we dont protect this a
malicious user could send a request to set the admin flag to true, giving them unauthorized privileges
on the site. With StrongParams, an unauthorized admin flag would be discarded.
And with this our specs should again be green.
Lets commit
Commit changes
1
2
git add.
git commit -m 'Enable saving of contact'
There are a couple more things that we will do before we wrap up this chapter, allowing users to
cancel creation of a contact and deleting an existing contact.
Well start with cancelling.
61
spec/javascripts/integration/contacts_integration_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
Here we transition to the add_new_contact view, check that we are there. We then click the cancel
button and check we have return to the original view. As can be see here, we can nest the andThen
calls.
We should update the template now.
/app/assets/javascripts/templates/contacts/index.hbs
1
2
3
4
5
<!-- =-->
<button id="save_new_contact" {{action 'saveNewContact'}} >Save new contact</butt\
on>
<button id="cancel_new_contact"{{action 'cancelNewContact'}}>Cancel new contact</\
button>
This pattern should look pretty familiar now. Our spec will now complain that Nothing handled
the action 'cancelNewContact', and im sure you know now how to fix that up.
/app/assets/javascripts/controllers/contacts_index_controller.js
1
2
3
4
5
6
7
//.....
},
cancelNewContact: function () {
this.set('new_first_name', '');
this.set('new_last_name', '');
this.toggleProperty('addingNewContact');
}
62
Deleting a Contact
The final thing well do in this chapter is add the ability to delete a contact from our list.
Failing spec time.
spec/javascripts/integration/contacts_integration_spec.js
1
2
3
4
5
6
7
8
//....
test('Deleting a contact', function () {
visit('/contacts').click('.contacts_list li:first .delete_button');
andThen(function () {
var contacts = find('ul li').length;
ok(contacts == 1, "Exepcted 1 contact got: " + contacts);
});
});
So we visit contacts, click on the first delete button and then expect our list of contacts to shrink to
1.
/app/assets/javascripts/templates/contacts/index.hbs
1
2
3
4
5
6
7
{{#each}}
<li>
{{#link-to 'contacts.show' this}}{{first_name}}{{/link-to}}
<button {{action 'deleteContact' this}} class="delete_button">Delete \
contact</button>
</li>
{{/each}}
The only real difference with this call is that when we specify the deleteContact action we also
pass in this, which makes the current object available in the controller.
We can use the Ember Datas destroyRecord function to remove the record from the store and send
a delete request to the server.
63
/app/assets/javascripts/controllers/contacts_index_controller.js
1
2
3
4
5
//....
},
deleteContact: function (contact) {
contact.destroyRecord();
}
contact is received from the template and we simply make the correct function call on it.
At this point youll notice that our spec is still failing and the reason is our FIXTURES dont get
reset between tests. When we added our Buzz Light year, we upped the number to 3, and with the
delete its down to 2. Grr!
In order to resolve this issue, it would be good to know that we start each integration spec with a
clean data set. Well do a spot of refactoring now
spec/javascripts/spec_helper.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
resetFixtures();
We are now wrapping the setup of the FIXTURES in a function and immediately calling the function.
This means our other tests are not disrupted, but we can now reliable return our data to baseline in
the integration specs. Lets do that now.
64
spec/javascripts/integration/contacts_integration_spec.js
1
2
3
teardown: function () {
resetFixtures();
}
describe Api::V1::ContactsController do
#..
describe 'DELETE method' do
it 'deletes a contact' do
@contact = FactoryGirl.create(:contact)
expect{delete :destroy, id: @contact.id}.to change(Contact, :count).by(-1)
end
end
3
4
#....
5
6
7
8
9
10
11
12
def destroy
if @contact.destroy
render json: nil
else
render json: @contact.errors, status: :unprocessable_entity
end
end
All is well on our specs, but well still have a problem if we try to delete a contact in our real app
and this is due to Cross Site Request Forgery protections that Rails adds by default. We will solve
this the Ember Appkit Rails way.
Ember Appkit Rails is a great project aimed at helping you get up and running with Ember and Rails by setting sensible defaults and providing
you with access to generators. https://github.com/dockyard/ember-appkit-rails/
65
/app/assets/javascripts/application.js.erb
1
2
/app/assets/javascripts/csrf.js
1
2
3
4
5
6
7
8
9
This takes the CSRF token from the page header and adds it to all of our ajax requests. And with
that, we should be able to add and delete contacts.
Time for a commit
Commit changes
1
2
git add .
git commit -m 'Add and remove contacts'
A good place would be to start with a high level failing integration spec.
Whilst this could quite easily be added to the contacts_integration_spec.js, well create a new
file to keep our tests tidy.
2
3
4
5
6
7
8
9
10
11
With this spec were looking for elements with the email_address class and checking to see how
many exist. Were also checking to specify that the first element contains specific text. The reason
for the two types of test will become apparent later.
Running our spec well get a failure
For more information on relationships in Rails visit http://guides.rubyonrails.org/association_basics.html#the-types-of-associations
67
Failure message
1
Failures:
2
3
4
At this stage well live with this failing spec for a while as we move to implement the infrastructure
required to get it to green.
module('Email model');
2
3
4
5
Well call the model email for simplicity, so we look for an email model, with an address attribute
of type: string. We should have a failure similar to this:
Failure message
1
2
3
4
5
68
AddressBook.Email = DS.Model.extend({
address: DS.attr('string'),
contact_id: DS.attr('number')
});
Testing Relationships
Now we will need to have a relationship between our contact and our email addresses. Each contact
may have zero or many email addresses, so lets write a spec for that.
spec/javascripts/models/contacts_spec.js
1
2
3
4
5
6
7
8
//....
});
test('relationships', function () {
var emails = AddressBook.Contact.metaForProperty('emails');
ok(emails.isRelationship, 'Expecting isRelationship to be true, got false');
equal(emails.kind, 'hasMany', 'Expected a hasMany relationship got: ' + email\
s.kind);
});
With this test were are using some new concepts. metaForProperty('emails') looks for an emails
property on our model. We then test to see if .isRelationship is true. .kind returns a string
describing the relationship.
We will expect to see a failure similar to this:
Failure message
1
Failures:
2
3
4
5
6
7
We can implement the relationship now by adding a new property to our Contact model.
69
/app/assets/javascripts/models/contacts.js
1
2
last_name: DS.attr('string'),
emails: DS.hasMany('email')
And with that we should have a green spec for our relationship.
Rendering Relationships
Well now need a place to display our data so we need to update the show template.
/app/assets/javascripts/templates/contacts/show.hbs
1
2
3
4
5
6
7
8
</ul>
<ul>
{{#each emails}}
<li class="email_address">
{{address}}
</li>
{{/each}}
</ul>
Now in order to get our integration spec passing we will need some sample data to render. We will
need to update our fixtures in order to do this and in doing so well discover a new gotcha.
spec/javascripts/spec_helper.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
},
{
16
17
id: 2,
address: '[email protected]',
contact_id: 1
18
19
20
21
];
22
23
70
};
With this update to the fixtures we add an emails property to our first contact which specifies the
ids of the associated emails. This will mean our first contactct should have two associated emails,
whilst the second one still has zero.
We should now get a spectacular explosion with multiple specs failing with a similar message!
Failure message
1
2
3
4
Now this seems to be telling us exactly how to fix the issue, so we can try that:
/app/assets/javascripts/models/contacts.js
1
2
//..
emails: DS.hasMany('email', {async: true})
Most of the failures will now be fixed, but our integration spec is still complaining.
Failure message
1
Failures:
2
3
4
So we now have two elements on the page for email addresses, but we dont have the correct text.
The fix for this turns out to be kinda obscure. Im not entirely sure that this approach would be
considered best practice and it feels like a bit of a hack.
With that in mind, lets remove the change to contacts.js
71
/app/assets/javascripts/models/contacts.js
1
2
//...
emails: DS.hasMany('email')
address: '[email protected]'
];
AddressBook.Contact.reopen({
emails: DS.hasMany('email', {async: true})
});
//...
3
4
5
6
7
8
};
What were doing here is overwriting the hasMany property after the fixtures are created
which for some reason fixes the issue! I feel like this is the right place to make the hack as
it will be easy to remove if this issue gets resolved in the future.
git add .
git commit -m 'Add Email model and relationship'
Gemfile
1
2
3
gem 'rspec-rails'
gem 'shoulda-matchers'
end
bundle install
An Email Model
Well need an email model, so lets start with a failing spec.
New file - spec/models/email_spec.rb
1
require 'spec_helper'
2
3
4
describe Email do
let(:email) { FactoryGirl.build_stubbed(:email)}
5
6
subject { email }
7
8
9
10
11
end
72
73
Failures:
2
3
4
5
6
7
8
9
1) Email
Failure/Error: let(:email) { FactoryGirl.build_stubbed(:email)}
ActiveRecord::StatementInvalid:
Could not find table 'emails'
# ./spec/models/email_spec.rb:4:in `block (2 levels) in <top (required)>'
# ./spec/models/email_spec.rb:6:in `block (2 levels) in <top (required)>'
# ./spec/models/email_spec.rb:9:in `block (2 levels) in <top (required)>'
In order to get this passing well need a table to store the emails, and well create a migration for
that.
New file - /db/migrate/create_emails.rb
1
2
3
4
5
6
7
8
If youre fairly new to rails, the t.references may be new to you. This statement tells Rails that
there is going to be a relationship with contacts and when the migration is run, it will create a
column called contact_id. This is just a more expressive way of saying t.integer :contact_id.
There are no database level foreign key constraints created.
We can now run the migration.
Migrating
1
2
Our failure message should now be complaining about the missing factory.
Failure message
1
2
3
4
1) Email
Failure/Error: let(:email) { FactoryGirl.build_stubbed(:email)}
ArgumentError:
Factory not registered: email
FactoryGirl.define do
factory :email do
address "MyString"
end
end
You will need to execute the reload command in the guard session now to make the new
factory available.
Contact Relationship
Our failure should now tell us what we need to fix up next.
74
75
Failure message
1
Failures:
2
3
4
5
6
7
Email Relationship
Well need to specify the relationship in the opposite direction now, so well add a new test to our
contact spec.
spec/models/contact_spec.rb
1
2
Failures:
2
3
4
5
6
7
76
/app/models/contact.rb
1
2
require 'spec_helper'
2
3
4
describe Api::V1::EmailsController do
end
spec/controllers/api/v1/emails_controller_spec.rb
1
2
3
4
5
6
7
8
describe Api::V1::EmailsController do
describe 'GET methods' do
it 'returns an email address' do
@email = FactoryGirl.create(:email)
get :show, id: @email.id
assigns(:email).class.should == Email
end
end
namespace :v1 do
resources :contacts
resources :emails
Failures:
2
3
4
5
6
7
8
77
/app/controllers/api/v1/emails_controller.rb
1
2
3
4
5
6
7
private
8
9
10
11
12
def set_email
@email = Email.find(params[:id])
end
end
Well also need a way to add an email, so lets get that sorted
spec/controllers/api/v1/emails_controller_spec.rb
1
2
3
4
5
6
7
8
9
Failure message
1
Failures:
2
3
4
5
6
7
78
/app/controllers/api/v1/emails_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ...
def create
@email = Email.new(get_email_params)
if @email.save
render json: nil
else
render json @email.errors, status: :unprocessable_entity
end
end
private
#...
def get_email_params
params.require(:email).permit([:address, :contact_id])
end
Failure message
1
2
3
4
5
79
80
/app/controllers/api/v1/emails_controller.rb
def destroy
if @email.destroy
render json: nil
else
render json @email.errors, status: :unprocessable_entity
end
end
1
2
3
4
5
6
7
With this new class defined, well get our other serializers to inherit from it. This tells Rails to use
a particular convention for including associations. Essentially the object were looking for specified
at the root level and its association ids are specified as an array. Also at the root level we get the
associated records as an array of objects. Ember data does the work to reassemble the data on receipt.
Embedding Format
1
2
3
4
5
6
7
8
9
10
11
12
{
"contacts": {
"id": 1,
"first_name": "Dave",
"last_name": "Crack",
"emails": ["1", "2"]
},
"emails": [{
"id": "1",
"address": "[email protected]"
}, {
"id": "2",
"address": "[email protected]"
13
}]
14
15
/app/serializers/contact_serializer.rb
1
2
3
4
With that weve got everything we need to display our associated emails on the frontend.
Phew, time for a commit!
Commit
1
2
git add .
git commit -m 'Emails and serialization'
81
//.....
var computedPropertyTest = function (model, record, computed_property, expected_o\
utput) {
var store = AddressBook.__container__.lookup('store:main');
Ember.run(function () {
var new_record = store.createRecord(model, record);
var computed = new_record.get(computed_property);
equal(computed, expected_output, 'Expected ' + expected_output +' got: ' \
+ computed);
});
};
expected input
example
model
'full_name'
record
computed_property
expected_output
83
// ....
});
test('full_name computed property', function () {
computedPropertyTest('contact', {first_name: 'Mabel', last_name: 'Smith'}, 'f\
ull_name', 'Smith, Mabel');
});
Failures:
2
3
4
emails: DS.hasMany('email'),
full_name: function () {
return this.get('last_name') +', '+ this.get('first_name');
}.property()
Beautiful, weve got the spec passing. It would be great, however if the output got updated when
one of its dependencies changes.
Lets test that.
spec/javascripts/unit/models/contacts_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
});
test('full name updates when properties change', function () {
var store = AddressBook.__container__.lookup('store:main');
Ember.run(function () {
var contact = store.createRecord('contact', {first_name: 'Buzz', last_nam\
e: 'Lightyear'});
var full_name = contact.get('full_name');
equal(full_name, 'Lightyear, Buzz', 'Expected "Lightyear, Buzz", got: ' +\
full_name);
contact.set('first_name', 'Slinky');
full_name = contact.get('full_name');
equal(full_name, 'Lightyear, Slinky', 'Expected "Lightyear, Slinky", got:\
'+ full_name);
contact.set('last_name', 'Dog');
full_name = contact.get('full_name');
equal(full_name, 'Dog, Slinky', 'Expected "Dog, Slinky", got: '+ full_nam\
e);
});
});
Failures:
2
3
4
5
6
7
84
85
/app/assets/javascripts/models/contacts.js
1
2
3
full_name: function () {
return this.get('last_name') +', '+ this.get('first_name');
}.property('first_name', 'last_name')
Lovely, green specs again. Now it would be nice to use the full_name property in our contacts index.
spec/javascripts/integration/contacts_integration_spec.js
1
2
3
4
5
6
7
8
9
So we can now update our index template to use the new computed property instead for first_name.
/app/assets/javascripts/templates/contacts/index.hbs
1
2
git add .
git commit -m 'Computed properties'
cd /spec/javascripts/support
wget https://github.com/trek/FakeXMLHttpRequest/raw/master/fake_xml_http_request.\
js
wget https://raw.github.com/trek/fakehr/master/fakehr.js
wget https://raw.github.com/trek/ember-testing-httpRespond/master/httpRespond-1.1\
.js
With these files downloaded, lets add them to the dependencies for teaspoon.
87
spec/javascripts/spec_helper.js
1
2
3
4
5
6
//=
//=
//=
//=
//=
//=
require application.js.erb
require support/fake_xml_http_request
require support/httpRespond-1.1
require support/fakehr
require support/testing_helpers
require_self
By including these three files we make a new construct available in our testing, .httpRespond("request
type", "url", response as object or array of objects) . The helper can be chained from
.click() and other testing contracts to mock a server hit and response.
Lets see how this works by creating a new spec for frontend email testing.
New file - spec/javascripts/integration/frontend_email_spec.js
1
2
3
4
5
6
7
8
module('Frontend Email', {
setup: function () {
fakehr.start();
},
teardown: function () {
fakehr.stop();
}
});
This first step should look fairly familiar, with the addition of the fakehr start and stop calls. As the
name suggests, it starts and stop the HTTP faker!
We should now write our failing spec.
spec/javascripts/integration/frontend_email_spec.js
1
2
3
4
5
6
7
8
9
10
});
//..
test("Add an email", function () {
var fakeContact = {contact: {id: 1, first_name: 'Dave', last_name: 'Crack', e\
mails: [1, 2]}, emails: [
{id: 1, address: '[email protected]'},
{id: 2, address: '[email protected]'}
]};
visit('/contacts/1').httpRespond('get', '/api/v1/contacts/1', fakeContact);
});
88
So theres a lot going on here! First we create an object that will represent our contact using the
format that Rails will send it in:
Top level - a contact with first name and last name strings, and an emails array which contains
two ids
Top level - an array containing email objects
From there we visit the correct route /contacts/1 and add the new httpRespond() function. We tell
it to expect a get request to /api/v1/contacts/1 and to return the fakeContact. Now we havent
actually issued any expectations at this point, but lets run the test anyway.
Failure message
1
Failures:
2
3
4
5
When we use the httpRespond() function is sets an expectation that it will be called. Now we know
that when we visit /contacts/1 Ember should be requesting data from the server, so whats gone
wrong? Well the problem is that we earlier setup a FixtureAdapter which means that no server
requests are happening!
In order to resolve this were going to ensure we have functions to turn on the RESTAdapter or the
FixtureAdapter as necessary. Well start by moving the resetFixtures function our of our spec_helper
(note that the line above it goes too!).
spec//javascripts/spec_helper.js
1
AddressBook.ApplicationAdapter = DS.FixtureAdapter;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
89
//..
var resetFixtures = function () {
AddressBook.ApplicationAdapter = DS.FixtureAdapter;
AddressBook.Contact.FIXTURES = [
{ id: 1, first_name: 'Dave', last_name: 'Crack', emails: [1, 2] },
{ id: 2, first_name: 'Dustin', last_name: 'Hoffman' }
];
AddressBook.Email.FIXTURES = [
{ id: 1, address: '[email protected]', contact_id: 1 },
{ id: 2, address: '[email protected]', contact_id: 1 }
];
AddressBook.Contact.reopen({
emails: DS.hasMany('email', {async: true})
});
};
Next well ensure that we turn have the fixture adapter turned back on after our frontend_email_spec.
spec/javascripts/integration/frontend_email_spec.js
1
2
3
teardown: function () {
fakehr.stop();
resetFixtures();
90
spec/javascripts/support/testing_helpers.js
1
2
3
//..
var turnOnRESTAdapter = function () {
AddressBook.ApplicationAdapter = DS.RESTAdapter;
AddressBook.Store = DS.Store.extend({
adapter: '-active-model'
});
5
6
7
8
DS.RESTAdapter.reopen(
{namespace: "api/v1"}
);
9
10
11
12
};
What we are doing here is specifying that the ApplicationAdapter uses the DS.RESTAdapter, telling
the Store to use the active-model structure for serialising the data and telling the RESTAdapter to
prepend the AJAX calls with api/v1.
We can now use this function in the frontend_email_spec.
spec/javascripts/integration/frontend_email_spec.js
1
2
3
setup: function () {
turnOnRESTAdapter();
//..
Great, now we should see QUnit complaining about no assertions being specified.
Failure message
1
Failures:
2
3
4
5
Now Ive specified in our mock JSON data that we should have an email [email protected] which
is different to our Fixture data, lets test for that to make sure the data is getting rendered.
91
spec/javascripts/integration/frontend_email_spec.js
1
2
3
4
5
So here we introduce a new way of checking that text can be found in the page by using a
RegularExpression test. This takes in a jQuery object on which we call text() and returns true if the
text is found. The same effect can be achieved with the find helper, but its great to have choices!
With that, our tests should be green.
Now lets start adding the behaviour we want to add a new email.
Weve seen earlier that we can use a boolean to set a state and use this in the view to determine
what is rendered, so lets create the behaviour we want attached to a button which will then render
an input field where the user can enter a new email address.
Well start with the button.
spec/javascripts/integration/frontend_email_spec.js
1
2
3
4
We are asking for or spec to look for a element with an id of create_email, click the button and
then find an element with an id of new_email.
With that we should be getting a similar error to this
Failure message
1
Failures:
2
3
4
92
/app/assets/javascripts/templates/contacts/show.hbs
1
2
3
4
5
6
{{#if addingEmail}}
{{else}}
<button id="create_email" {{action 'createEmail'}} >Add new email</button>
{{/if}}
<ul>
{{#each emails}}
So here we are stating that we should check truthiness of addingEmail and if its false, display a
button. We assign the action createEmail to the button.
Our tests should now be guiding us where to go.
Failure message
1
Failures:
2
3
4
5
6
Now the correct place to handle this behaviour would be in a ContactShowController, so lets get
that created.
New file - /app/assets/javascripts/controllers/contacts_show_controller.js
1
2
3
4
5
6
7
AddressBook.ContactsShowController = Ember.ObjectController.extend({
actions: {
createEmail: function () {
this.toggleProperty('addingEmail');
}
}
});
We use the Ember.ObjectController.extend constructor for this controller because we are dealing
with a single item, rather than a collection.
Our tests should now be telling us that the input field cannot be found.
93
Failure message
1
2
{{#if addingEmail}}
<label for="new_email">Email address</label>
{{input type='text' value=controller.new_email id='new_email'}}
Here we create a label element and a text input field. We give the input field an id of new_email
and bind its value to the controller under a new_email property.
Now notice that we didnt need to specify a new_email object on the controller or to set a boolean
or addingEmail, Ember deals with this for us when the actions are called.
Our tests should again be green.
We will now specify the behaviour we would like to see when a user tries to save the new email.
spec/javascripts/integration/frontend_email_spec.js
1
2
3
4
Expected input
selector
type
keyCode
We now know that we are expecting the enter button to be pressed to save the email. This seems
like a nice, user focussed thing to do - rather than adding another button for them to click and also
A list of many of the available key codes is here http://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes
94
gives us the opportunity to look at some of the other things we can test!
Our httpRespond this time is expecting to receive a POST request to api/v1/emails and will return
with an empty response with a 200 (success) status code.
And with that, we have a new failure indicating that no request has been made.
Failure message
1
Failures:
2
3
4
5
Theres a few things we need to get this resolved. First we need to bind the enter key to an action.
/app/assets/javascripts/templates/contacts/show.hbs
1
2
3
4
{{#if addingEmail}}
<label for="new_email">Email address</label>
{{input type='text' value=controller.new_email id='new_email' insert-newl\
ine='saveNewEmail'}}
Saving Associations
This bit is fairly involved, so Ill explain it in more detail afterwards.
/app/assets/javascripts/controllers/contacts_show_controller.js
},
saveNewEmail: function () {
1
2
3
//1
var self, emailStore, newEmail, contactID, record;
4
5
//2
self = this;
6
7
//3
emailStore = this.store;
8
9
10
//4
newEmail = this.get('new_email');
//5
contactID = this.get('id');
12
13
//6
14
15
16
17
18
19
20
21
22
23
24
25
95
1.
2.
3.
4.
5.
96
spec/javascripts/integration/frontend_email_spec.js
1
2
3
4
5
6
Failures:
2
3
4
We can fix that by pushing the data we receive from the server into our store.
/app/assets/javascripts/controllers/contacts_show_controller.js
1
2
3
self.set('new_email', '');
var localEmails = self.get('model.emails');
localEmails.pushObject(data);
97
spec/javascripts/integration/frontend_email_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Here we specify that when we hit the enter button we expect that a post request is made and that
it will return a black response with a 400 failure code. This should trigger the display of a message
telling the user that the save failed.
To implement this well need a place in our template to render the message.
98
/app/assets/javascripts/templates/contacts/show.hbs
1
2
3
4
5
6
{{#if failedToSave}}
<div>
<p class="error">{{failedToSaveMessage}}</p>
</div>
{{/if}}
{{#if addingEmail}}
localEmails.pushObject(data);
};
var onRejection = function () {
self.toggleProperty('failedToSave');
self.set('failedToSaveMessage','Failed to save email, please try later');
};
record.save().then(onFulfillment, onRejection);
With this we creating a failedToSave boolean and setting it to true and creating a failedToSaveMessage
string. Green specs again, but what happens if the user then proceeds to try and save again and this
time it succeeds?
Lets update our spec to allow for a success following failure.
spec/javascripts/integration/frontend_email_spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
99
Now that were testing the text twice we pull it out into a variable, then we add another keyEvent
with a successful response and assert that we should not see the error message any more.
/app/assets/javascripts/controllers/contacts_show_controller.js
1
2
3
4
5
6
7
8
9
10
11
And with that weve got a useful message for our users if the save fails and way of dealing with
success after failure.
Now would be a good time to commit our changes before we move on to connecting this up to the
backend.
Commit changes
1
2
git add .
git commit -m 'Frontend email specs and implementation'
100
spec/controllers/api/v1/emails_controller_spec.rb
1
2
3
4
5
6
7
8
9
10
11
12
Here we are using a tiny bit of meta-programming to make the code a little bit shorter. This also
means we can easily update the test in the future if we want to make any changes.
Failure message
1
Failures:
2
3
4
5
6
7
8
This is because we are not actually returning any json currently, only a success code. Lets get that
sorted, and a I apologise for how much work this is going to take.
/app/controllers/api/v1/emails_controller.rb
1
2
3
4
def create
@email = Email.new(get_email_params)
if @email.save
render json: @email
Commit changes
1
2
git add .
git commit -m 'Creating emails specs and implementation'
In the next chapter well have a look at updating our contacts and their email addresses.
101