105

EmberJS Testing on Rails

Embed Size (px)

DESCRIPTION

Testing EmberJS app in Ruby On Rails

Citation preview

Page 1: EmberJS Testing on Rails
Page 2: EmberJS Testing on Rails

Ember.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 Publishingprocess. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools andmany iterations to get reader feedback, pivot until you have the right book and build traction onceyou do.

©2014 Martin Feckie - RN, MHSM

Page 3: EmberJS Testing on Rails

Contents

Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

Erratum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2

Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4A Bit About My Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4First There Was Rails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5Along Came jQuery . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5Javascript IV -Ember - A New Hope . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6Testing to the Rescue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

Design Choices, the Golden Path and ‘Why Didn’t You Include / Use / Show X, Y, Z?” . . 8

Chapter One - Bootstrapping Our App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9What are we going to build? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9Prerequisites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9Generating Our App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9Getting Rid of Some Junk . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10Some Additional Development Dependencies . . . . . . . . . . . . . . . . . . . . . . . . . 12Install Ember . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13

Chapter Two - Testing Setup . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17RSpec . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17Teaspoon . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

Chapter Three - Routing Specs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24QUnit Specs (or Are They Tests?) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24Creating a Contacts Route . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27And So a Problem Presents Itself . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28An Abstraction Trying to Get Out? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

Chapter Four - Getting Started With Data Rendering . . . . . . . . . . . . . . . . . . . . . 32Models . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33Unit Testing Our Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35An Unexpected Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37

Page 4: EmberJS Testing on Rails

CONTENTS

Chapter Five - Nested Routes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39Defining Routes Within Resources . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40Rendering the Individual Contact . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41Refactor Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41An Alternate Way of Viewing Our Specs . . . . . . . . . . . . . . . . . . . . . . . . . . . 42Navigating Around the App . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43

Chapter Six - Persisting Data With Rails . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45Setting Up the Backend . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45Serializing the Data . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50Connecting the Backend to the Frontend! . . . . . . . . . . . . . . . . . . . . . . . . . . . 51More Routes Necessary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52Adding Contacts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54Deleting a Contact . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62

Chapter 7 - Relationships in Ember . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66A Place to Store Emails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67Testing Relationships . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68Rendering Relationships . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69Wiring Up the Relationships to Rails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71An Email Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72Contact Relationship . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74Email Relationship . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75Email Controller and Routes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76Serializing and Relationships . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80

Chapter 8 - Unit Testing Computed Properties . . . . . . . . . . . . . . . . . . . . . . . . . 82

Chapter 9 - Creating and Updating Emails on the Frontend . . . . . . . . . . . . . . . . . 86Faking HTTP Requests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86Reimplementing RESTAdapter in Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89Saving Associations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94Dealing With Failed Saves . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96Testing JSON Responses in Rails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99

Page 5: EmberJS Testing on Rails

AcknowledgementsI would like to acknowledge the special contribution of Javier Cadiz for the extremely useful feedbackand for test driving the book.

The Ruby Rogues and their amazing guests. I’ve 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 Crockford’s series Crockford on JavaScript¹ should be watched by anyone interested inthe language, entertaining, insightful and really brilliant at explaining the good bits and the bits toworry about.

Sandi Metz for Practical Object Oriented Design in Ruby and every one of her talks available on theinternet.

Ryan Bates for the RailsCasts series. Giving such a great resource away is a true act of altruism.

¹http://www.yuiblog.com/crockford/

Page 6: EmberJS Testing on Rails

ErratumChapter 7

Contact class should destroy emails when contact is destroyed.

contact.rb

1 class Contact < ActiveRecord::Base

2 has_many :emails, dependent: :destroy

3 end

Chapter 7

Serializer should not include has_one :contact

email_serializer.rb

1 class EmailSerializer < ApplicationSerializer

2 attributes :id, :address, :contact_id

3 end

Chapter 7

contact_id was missing from model.

New file - /app/assets/javascripts/unit/models/email.js

1 AddressBook.Email = DS.Model.extend({

2 address: DS.attr('string'),

3 contact_id: DS.attr('number')

4 });

Chapter 7

contact_id missing form FIXTURES.

Page 7: EmberJS Testing on Rails

Erratum 3

spec/javascripts/spec_helper.js

1 var resetFixtures = function () {

2 AddressBook.Contact.FIXTURES = [

3 {

4 id: 1,

5 first_name: 'Dave',

6 last_name: 'Crack',

7 emails: [1,2]

8 },

9 //.....

10 ];

11 AddressBook.Email.FIXTURES = [

12 {

13 id: 1,

14 address: '[email protected]',

15 contact_id: 1

16 },

17 {

18 id: 2,

19 address: '[email protected]',

20 contact_id: 1

21 }

22 ];

23 };

Page 8: EmberJS Testing on Rails

IntroductionA Bit About My Motivation

I’ve been very interested in the rise of Ember.js and front end web frameworks in general. I recentlymade an app for a customer using Angular.js for front-end rendering. There is a very clear focus ontesting in Angular and I found it really easy to build a test suite. Integrating with Rails was muchmore of a challenge however. I began to look at Ember and gave it a good go. At the time I foundthat persisting data was a real challenge as was integrating with Rails, this was a big disappointmentand in the end I kept going with Angular and was able to produce a well tested app I was happywith. Angular, however, is much more free form than Ember and presents lots of opportunities fordifferences of opinion and approach. Not necessarily a bad thing, but if I’m producing an app I’dlike another developer to be able to come along and have a very good idea of where to find thingsand Ember presents a much higher opportunity to achieve this.

I’ve continued to watch the development of Ember and LOVE where it’s going, the passion andtalent of its developers is amazing and I’m so grateful for what they are doing. Most of my initialdifficulties around persistence have been well and truly resolved. I’m still, however, frustrated withthe lack of documentation about testing beyond integration tests. There are heaps of blog posts ourthere and people have put a lot of work into finding ways to test Ember apps. The problem is, withall the API changes, many are outdated and simply don’t apply any more. That shouldn’t be takenas a criticism of any of the work the authors have put in, I’m truly grateful they did.

I had desperately hoped to find a book that would tell me how to do test driven development withEmber, but it still hasn’t eventuated. I’ve spent months playing around with different configurationsand setups with Rails and Ember and came to the realisation that in creating my own setup I’velearned a lot. As a result I thought I would try and write the book I would like to have read!

I’m not gonna suggest for a minute that everything I’ve put forward is perfect, I’m sure it’s not andthere will definitely be others who will come up with better ideas in the future. I’m content thatwhat I’ve put forward is a good starting point. I’m very open to feedback and dialogue and want tocreate something that helps. If I succeed in helping, please tell others, if not please tell me!

Page 9: EmberJS Testing on Rails

Introduction 5

A Note on CopyrightIf you bought this book or were given a copy by me, skip this!

If you didn’t 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 experiencewith 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 receivesregular updates.

Karma is a wonderful thing and if you do choose to take my book without paying, then Iwish you well and I hope that you find it useful and it helps you develop in your career. Ifso, please consider buying a copy for someone else who would benefit.

First There Was Rails

If you’ve even thought about purchasing this book, I’m sure you’re familiar with Ruby on Rails. Asa framework it’s done much to drive best-practices in web application development, providing userswith a truly open source framework very different from some of the proprietary systems on offer.

The true beauty of working on a Rails app is that any part of the framework that you don’t like, youcan change! Monkey patch away, but be warned, straying from the ‘golden path’ is something thatcan really cause you long term pain.

To combat some of the perils of free form development Rails provides us with a convention overconfiguration philosophy. If you do things ‘the Rails way’ then things tend to flow very easily andyour pain is minimized!

One of the conventions that I love the most is the focus on testing. There are plenty that argue thatthe style of Test Drive Development provided by Rails is invalid. The argument goes that it doesn’tfollow the test first style recommend by many. When you run

1 $> rails generate model Thing name:string

you get pre populated tests for your newly generated model. Personally, I think that’s a good thingfor new developers and it’s 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 andeveryone was happy. But then…

Along Came jQuery

Despite masses of critics, there can be no denying that javascript won the language wars (in terms ofavailability at least!). It’s used onmore devices, in more places that any other language.We can spendhours bike shedding the topics, or accept reality and find good ways to work with the language.

Page 10: EmberJS Testing on Rails

Introduction 6

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 thingswith javascript - anyone rememberthe pop-up laden websites of the early 2000’s? Not only did javascript facilitate annoying pop-up’sbut also helped malicious actions.

Having said all that, some people found really creative uses for javascript and one of the mostsuccessful early ideas was jQuery. jQuery provided developers with a straightforward way tointeract with the Document Object Model (DOM). Developers being lazy (in a good way) foundthe $ abstraction very useful.

Javascript DOM selection vs jQuery

1 document.getElementByClassName('container')

2 document.getElementById('someID')

3 document.getElementByTagName('p')

4 // vs

5 $('.container')

6 $('#someId')

7 $('p')

In and of itself, the $ abstraction doesn’t do anything to speed up the interface, but does aid devel-opers in providing a one stop shop for interacting with the DOM. Not needing to change methodsspeeds development sowe don’t 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 applicationgrew to a reasonable size, it became a fight to keep the code clean and developers would oftenexperience call-back hell ².

Javascript IV -Ember - A New Hope

I knew that Ember.js was going to be special the moment I looked at the early website. I can’t believehow excited I was by the bindings! Oh the bindings! I type it here and it’s updated there, live and Ican persist it!!! Woo hoo.

It’s not surprising that Ember is such a great framework, after all it’s got Yeduda Katz (of jQuery,Rails and much more), Tom Dale. The other core contributors are amazing as are the hundreds ofother who’ve made contributions. I’ve got a tremendous amount of respect for the Ember team, theircommitment to getting it right and their willingness to acknowledge when things went wrong (earlystage ember-data anyone?) is impressive.

²Call-back hell is a term used to refer to the problem to heavily nested code, often detected through it’s particular ‘triangular’ shape. http://callbackhell.com/

Page 11: EmberJS Testing on Rails

Introduction 7

I can also see the direction the project is going in and can see huge strides in performance andconvenience with the release of 1.0 . The six week release cycle will see the speed of the frameworkimprove and minor bugs and problems resolved (though already the benchmarks are impressive incomparison to other frameworks ³)

The learning curve for any framework following the convention over configuration pathway isalmost always huge. This is certainly the case with Ember. The initial excitement of being able todo so much, with so little code soon gives way to the frustration of an error caused by a misnamedclass or incorrect pluralisation.

Testing to the Rescue

Testing is a great way for us to explore the framework and I hope to provide a robust guide to allowyou to get up and going with an environment that provides rapid feedback and some good strategiesfor testing your Ember.js apps.

A word of warning on the codeexamplesThe Leanpub platform automatically creates a ‘\’ at the end of long lines to indicatewrapping. These obviously shouldn’t be copied and I haven’t found a way to turn themoff. Please be cautious when following the examples.

³Although artificial benchmarks are frowned upon, here’s 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

Page 12: EmberJS Testing on Rails

Design Choices, the Golden Path and‘Why Didn’t You Include / Use / ShowX, Y, Z?”I’ve put out a few releases of the book and so far have received positive feedback, however I’ve alsohad some questions that I feel are worth responding to.

Why Didn’t You Use Third Party Libraries to Cover BDD, Such asPavlov?

The philosophy I’m coming from with the book is that getting as ‘close to the metal’ as possible willgive you the knowledge and confidence to use third party libraries because you will learn what theyare abstracting away. Knowing the hooks they use and the methods they leverage will allow you totroubleshoot and make the trade offs you feel are worth it.

Why Don’t You Use Factories Instead of Fixtures in Ember?

Basically, none of the things we’re going to use fixtures with require any dynamic attributes. Fixturesare lightweight and provided for free by Ember. I chose not to add in the extra work to providedynamic objects.

You Know That You Can Run Multiple Suites of Tests WithTeaspoon?

Yes, in the first draft of the book I utilised this feature of Teaspoon, but found that there was aconflict with Guard that led to tests getting ‘stuck’ in one place, leading to false positive / negativenotifications via Growl. As a result, I chose not to use the feature because I valued reliability higherthan separation of unit and integration tests. The issue with ‘sticking’ may well get ironed out withfuture (or even current) releases of Guard / Teaspoon, so by all means use the feature yourself.

The Golden Path

I’ve chosen to use the same tools as much as possible as the Ember core team. If you prefer to useJasmine, Mocha, Chai, Pavlov etc. then go for it. Using QUnit would not be my first choice, but it iswell integrated with Ember, well supported on Stackoverflow and other sources of knowledge andprovides a great baseline. This is really about getting started with ‘training wheels’, if you’re readyto go without them, take them off an go!

Page 13: EmberJS Testing on Rails

Chapter One - Bootstrapping Our AppWhat are we going to build?

In this chapter we will get started with a brand new app and build a continuous testing setup thatallows us to create our app in confidence.

Come back and insert diagram of what’s gonna be built and explain some choices further

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 youwant to spend at least 30 minutes compiling form source!

Generating Our App

Lets get started by initialising a new app. We’ll be sticking with sqlite3 for this app because we don’thave to worry about user permissions and interfering with any postgres or mysql setup you mayhave on your system.

1 $> rails new address-book -T

The -T flags tells Rails not to include MiniTest, as we’ll be using RSpec

Next we’ll initialise a git repository.

Page 14: EmberJS Testing on Rails

Chapter One - Bootstrapping Our App 10

Initializing git repository

1 $> cd address-book

2 $> git init

3 $> git add .

4 $> git commit -m 'initial commit'

Getting Rid of Some Junk

One issue that we’ll run into is that turbolinks is included by default in Rails 4 and causes a significantnumber of conflicts, we’ll also avoid using CoffeeScript because there are aspects of its syntax thatdon’t comfortably fit with Ember code constructs ⁴. We’ll also get rid of jbuilder.

Removing unnecessary Gems from Gemfile

1 source 'https://rubygems.org'

2

3 gem 'rails', '4.1.0'

4 gem 'sqlite3'

5 gem 'sass-rails', '~> 4.0.0'

6 gem 'uglifier', '>= 1.3.0'

7 gem 'coffee-rails', '~> 4.0.0'

8 gem 'jquery-rails'

9 gem 'turbolinks'

10 gem 'jbuilder', '~> 1.2'

11

12 group :doc do

13 # bundle exec rake doc:rails generates the API under doc/api.

14 gem 'sdoc', require: false

15 end

16

17 group :development do

18 gem 'spring'

19 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 doesn’t alwaysmatchthese 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 aren’t valid CoffeeScript syntax, though there are workarounds http://discuss.emberjs.com/t/coffeescript-and-ember/2028 and a project that creates an Ember specific wrapper for CoffeeScript http://emberscript.com/

Page 15: EmberJS Testing on Rails

Chapter One - Bootstrapping Our App 11

Specifying the JQuery version

1 gem 'jquery-rails', '3.0.3'

Next, we’ll remove turbo link and jquery-ujs from the asset pipeline

app/assets/javascripts/application.js

1 //= require jquery

2 //= require jquery_ujs

3 //= require turbolinks

4 //= require_tree .

And finally we’ll remove reference to it in our templates.

app/views/layouts/application.html.erb

1 <head>

2 <title>AddressBook</title>

3 <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track"\

4 => true %>

5 <%= stylesheet_link_tag "application", media: "all"%>

6 <%= javascript_include_tag "application", "data-turbolinks-track" => true %>

7 <%= javascript_include_tag "application"%>

8 <%= csrf_meta_tags %>

9 </head>

Now we’ll remove the old gems using bundler. Note the --local flag, this tells bundler to only lookat things already installed locally and avoids hitting rubygems. This can be a great speedup, butobviously doesn’t help if your don’t have a particular gem already available locally.

Running bundler to remove unnecessary gems

1 $> bundle install --local

And finally we’ll commit our changes

Page 16: EmberJS Testing on Rails

Chapter One - Bootstrapping Our App 12

Committing changes

1 $> git add .

2 $> git commit -m 'Goodbye turbolinks'

Some Additional Development Dependencies

We’ll now add bunch of things that make error tracking better when developing our app.

• Quiet assets is great for reducing the amount of ‘noise’ in your server log during development.• Better errors⁵ gives you much better ways of dealing with errors, bringing up an interactiveconsole in browser when an error occurs. Being able to ‘dig around’ in the live code in thisstyle really helps to track down bugs.

• Meta request allows integration with RailsPanel in Google Chrome, which gives lots of usefulinformation about your requests, DB hits, and template rendering. I recommend you installthis from the Chrome web store if you do much Rails development.

• Binding of caller allow us to use a Read Evaluate Print Loop (REPL) in the interactive consolefor better_errors and helps with inspection of objects in the console.

Add the following to your Gemfile:

Gemfile

1 group :development do

2 gem 'spring'

3 gem 'quiet_assets'

4 gem 'meta_request'

5 gem 'better_errors'

6 gem 'binding_of_caller'

7 end

Then:

⁵See this great screencast by Ryan Bates on Better Errors and RailsPanel http://railscasts.com/episodes/402-better-errors-railspanel

Page 17: EmberJS Testing on Rails

Chapter One - Bootstrapping Our App 13

Install and commit

1 $> bundle install

2 $> git add .

3 $> git commit -m 'Add error handling gems'

Install Ember

Let’s get on now and install Ember. The core team have very kindly provided a great gem to help usintegrate Ember with rails.

Let’s add it to our Gemfile.

Adding Ember to our Gemfile

1 gem 'ember-rails'

2 gem 'ember-source', '1.5.0'

Ember-rails provides us with a lot of development dependencies such as the Ember.js files indevelopment and production versions, a precompilation pathway for Handlebars templates andActiveModel::Serializer, which we’ll use heavily for passing data back and forth between the server.

After we run bundler, we can use the build in generator to get Ember’s prerequisites installed.

Bootstrapping Ember

1 $> rails generate ember:bootstrap

2 insert app/assets/javascripts/application.js

3 create app/assets/javascripts/models

4 create app/assets/javascripts/models/.gitkeep

5 create app/assets/javascripts/controllers

6 create app/assets/javascripts/controllers/.gitkeep

7 create app/assets/javascripts/views

8 create app/assets/javascripts/views/.gitkeep

9 create app/assets/javascripts/routes

10 create app/assets/javascripts/routes/.gitkeep

11 create app/assets/javascripts/helpers

12 create app/assets/javascripts/helpers/.gitkeep

13 create app/assets/javascripts/components

14 create app/assets/javascripts/components/.gitkeep

15 create app/assets/javascripts/templates

16 create app/assets/javascripts/templates/.gitkeep

17 create app/assets/javascripts/templates/components

Page 18: EmberJS Testing on Rails

Chapter One - Bootstrapping Our App 14

18 create app/assets/javascripts/templates/components/.gitkeep

19 create app/assets/javascripts/mixins

20 create app/assets/javascripts/mixins/.gitkeep

21 create app/assets/javascripts/address_book.js

22 create app/assets/javascripts/router.js

23 create app/assets/javascripts/store.js

Whoah! That’s a whole bunch of setup that’s been done for us! What you’ll see from this is theinfluence of convention over configuration. There’s a place for everything and everything shouldbe in its place! At this stage, most of these files are only for place holding (.gitkeep tells git keep thefolder in the source control even if it is empty), except of the .js files. We’ll see what they are aboutin depth later.

Views vs TemplatesWhat you know as a view in Rails is almost certainly a template in Ember! It’s pretty easyto get caught out with these distinctions and I’m sure you’ll find yourself putting files inthe wrong place as you learn. If a page isn’t rendering as you expected, ask yourself if thefile is the correct directory.

Next we’ll ensure that we have the necessary Ember javascripts available.

Ember javascript dowload

1 $> rails g ember:install

This will go off and download the development and production javascripts necessary for Ember.

Now that we’ve got Ember available there are a few files that need updated to get us to a good basepoint.

We can now delete the app/view/layouts folder and its contents (application.html.erb).

..

You made me update it and now you’ve made medelete it!Yeah, some folks like to play along as we go, so I wanted to ensure that everything still works inbetween commits. Removing it earlier would have cause a render error, but also you now knowhow to get rid of turbolinks if you want to use a different application structure in the future.

Next, update your routes

Page 19: EmberJS Testing on Rails

Chapter One - Bootstrapping Our App 15

config/routes.rb

1 Rails.application.routes.draw do

2 root to: 'assets#index'

3 get 'assets/index'

4 end

With that route defined, we’ll need to create an associated controller.

app/controllers/assets_controller.rb

1 class AssetsController < ApplicationController

2 def index

3 end

4 end

We will also need a Rails view to render, so go ahead and create one.

app/views/assets/index.html.erb

1 <!DOCTYPE html>

2 <html>

3 <head>

4 <title>Address book</title>

5 <%= stylesheet_link_tag "application", :media => "all" %>

6 <%= csrf_meta_tags %>

7 </head>

8 <body>

9 <%= javascript_include_tag "application" %>

10 </body>

11 </html>

When using ember-rails you’re default starting point for rendering is a handlebars file calledapplication.hbs. This file will be rendered as your root route. Let’s create it.

app/assets/javascripts/templates/application.hbs

1 <h1>Welcome to the Address Book</h1>

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:

Page 20: EmberJS Testing on Rails

Chapter One - Bootstrapping Our App 16

Welcome to the Address Book

If all is well, let’s commit the changes.

Committing our Ember install

1 $> git add . --all

2 $> git commit -m 'Ember install'

If not, it’s time for some debugging! See you when you get back.

Page 21: EmberJS Testing on Rails

Chapter Two - Testing SetupWe’ll be using a combination of RSpec, QUnit, Guard, PhantomJS and Teaspoon for our testingenvironment. It’s taken a great deal of exploration to get to this combination and I now feelcomfortable sharing this setup with you.

..

Why not use Jasmine or {insert other frameworkhere}?Things I’ve tried that didn’t go so well (and why I abandoned them):

• jasmine-rails (not enough ‘community’ support for jasmine and Ember, so difficult to getadvice and examples)

• karma test runner (Didn’t like having to run two separate test suites)• capybara + selenium (Capybara is good, but quite verbose and doesn’t do too well withasynchronous testing)

• capybara + PhantomJS (Same as capybara + selenium)

I have used jasmine as my tool of choice for javascript testing when using Angular.js because Iparticularly like the describe, it does something style of test writing. It feels very comfortableand close to RSpec style. I spent a considerable period trying to get my setup going with jasmine,but in the end found that using the same tools as Ember core provided a happier experience andthere many more examples on the web to explore.

At the end of the day, you can use whatever you like with enough effort, but is it really worth it???

For those not familiar with PhantomJS, it’s a ‘headless’ browser based onWebkit. It allows us a verynice 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 andfind that RSpec provides a great way of thinking about my apps and as a result the tests flow frommy brain to the text editor very simply.

Getting started with RSpec is very straightforward and we will also turn off automatic specgeneration to avoid unnecessary kruft! We’ll also use FactoryGirl to for generating fixtures duringtests.

⁶For more information on DHH’s opinion http://www.rubyinside.com/dhh-offended-by-rspec-debate-4610.html

Page 22: EmberJS Testing on Rails

Chapter Two - Testing Setup 18

Add rspec-rails to Gemfile

1 group :test, :development do

2 gem 'factory_girl_rails'

3 gem 'rspec-rails'

4 gem 'spring-commands-rspec'

5 end

From Rails 4.1, spring is included by default, which is an amazing speed booster because it preloadsyour enviroment for you, significantly reducing the time taken to run migrations, generators andspec. By default it doesn’t support RSpec, but the spring-commands-rspec gem sorts that our for us.

We’ll now stop Rails from auto generating view, helper, controller and routing specs. Whilst thebuilt in routing and controller specs can be useful, I’ve found that they don’t work so well withnamespaced routes and controllers which we will use later. As a result I found that correcting themtook longer than writing from scratch. We will now update the application initialiser to prevent itfrom auto generating some specs when we use rails generators.⁷

config/application.rb

1 module AddressBook

2 class Application < Rails::Application

3 config.generators do |g|

4 g.test_framework :rspec, fixtures: true, view_specs: false,

5 helper_specs: false, controller_specs: false, routing_specs: false

6 g.factory_girl true

7 end

8 end

9 end

Now we can bootstrap RSpec.

Bootstrapping RSpec

1 bundle install

2 rails g rspec:install

Well commit these changes now.

⁷For more information on generators http://guides.rubyonrails.org/generators.html and http://railscasts.com/episodes/216-generators-in-rails-3

Page 23: EmberJS Testing on Rails

Chapter Two - Testing Setup 19

Committing changes

1 git add . -A

2 git commit -m 'Adding in RSpec'

Teaspoon

Teaspoon⁸ provides both a beautiful an elegant way interface for javascript testing on Rails, there’salso a plugin for Guard which means we can make automated testing very straightforward.

I particularly like keepingmy unit tests separate frommy integration tests. Teaspoon has the notationof suites allowing us to make simple switches. We can use fixtures or interact with live data fromour 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, let’s get started with Teaspoon, by adding it to our Gemfile inside the :test, :development

group. We’ll also take the opportunity to add in Guard and optionally Growl⁹.

Gemfile

1 group :test, :development do

2 # ...

3 gem 'growl' # Optional

4 gem 'guard-rspec'

5 gem 'guard-teaspoon', '0.0.4'

6 gem 'teaspoon', '0.7.9'

7 gem 'spring-commands-teaspoon'

8 gem "phantomjs", ">= 1.8.1.1" # this is optional if the phantomjs binary is ins\

9 talled on your system

10 end

We add in the support for spring commands with Teaspoon also to help with speeding up tests.

After running bundler we’ll initialise Teaspoon.

⁸Learn more about teaspoon here https://github.com/modeset/teaspoon⁹Growl is a really nice notifier application with lots of support and customisation. I really like that I can have my tests running in another window

and still get continuous feedback about how my tests are going. It’s only available for Mac though. http://growl.info/

Page 24: EmberJS Testing on Rails

Chapter Two - Testing Setup 20

Initializing Teaspoon

1 rails generate teaspoon:install

2 create spec/teaspoon_env.rb

3 create spec/javascripts/support

4 create spec/javascripts/fixtures

5 create spec/javascripts/spec_helper.js

6 +============================================================================+

7 Congratulations! Teaspoon was successfully installed. Documentation and more

8 can be found at: https://github.com/modeset/teaspoon

By default Teaspoon initialises with Jasmine, so lets fix that up.

spec/teaspoon_env.rb

1 //......

2 Teaspoon.configure do |config|

3 config.suite do |suite|

4 suite.use_framework :qunit

5 suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee\

6 }"

7 suite.helper = "spec_helper"

8 suite.stylesheets = ["teaspoon"]

9 end

10 end

One issue that crops up when using Teaspoon with Rails 4.1 is that asset pipeline issues are set toraise runtime errors in development mode. This causes issues with Teaspoon and can be resolved byadjusting turning off the runtime error mode.

config/initializers/development.rb

1 Rails.application.configure do

2 //.....

3 config.assets.raise_runtime_errors = false

4 ...

Now let’s convert our application.js to application.js.erb to give us a nice place to turn onand off Ember debugging flags.

Page 25: EmberJS Testing on Rails

Chapter Two - Testing Setup 21

app/assets/javascripts/application.js.erb

1 //= require jquery

2 //= require handlebars

3 //= require ember

4 //= require ember-data

5 //= require_self

6 //= require address_book

7

8 // for more details see: http://emberjs.com/guides/application/

9

10 <% if Rails.env.development? %>

11 Ember.LOG_TRANSITIONS = true;

12 <% end %>

13

14 AddressBook = Ember.Application.create();

15

16 //= require_tree .

We can now make changes in the development block that won’t affect our site in production.

Next we’ll add adjust the spec_helper.js to setup Ember for testing. Now there’s a fair bit goingon here. First we require the main application.js.erb then we add two div’s to allow a place forour 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 givesus access to a whole bunch of useful functions that we can use for testing¹⁰.

New file - spec/javascripts/spec_helper.js

1 //= require application.js.erb

2 //= require_self

3

4 var d = document;

5 d.write('<div id="ember-testing-container"><div id="ember-testing"></div></div>');

6

7 AddressBook.rootElement = "#ember-testing";

8 AddressBook.setupForTesting();

9 AddressBook.injectTestHelpers();

¹⁰More information on the run look and TDD here http://instructure.github.io/blog/2014/01/24/ember-run-loop-and-tdd/

Page 26: EmberJS Testing on Rails

Chapter Two - Testing Setup 22

In order to make our tests look a bit nicer when using web viewwe’ll add a splash of CSS. The stylingcomes from the Discourse project, one of the largest open source Ember apps in development¹¹.

New file - app/assets/stylesheets/teaspoon_custom.css

1 #ember-testing-container {

2 position: absolute;

3 background: white;

4 width: 640px;

5 height: 384px;

6 overflow: auto;

7 z-index: 9999;

8 border: 1px

9 solid #ccc;

10 right: 50px;

11 top: 200px;

12 padding: 5px;

13 }

14 #ember-testing { zoom: 50%; }

Now let’s tell Teaspoon to use this CSS.

spec/teaspoon_env.rb

1 suite.stylesheets = ["teaspoon", "teaspoon_custom"]

And now we will initalise the binstubs so that spring can help us out with running our tests quickly.

Generate binstubs

1 bundle exec spring binstub --all

There’s a fair bit of magic going on behind the scenes with this some of which is useful to know. Thefirst time you run rails after the binstubs are generated, spring fires up you app. You’ll see a deloyuntil the applicaiton is ready. Once spring is ‘up’, future commands will run more quickly. Springintelligently reloads any altered files in future, so you’ll find that any command run from here onwill get started much more quickly.

Guard

Before moving of to writing some actual tests (I know, it’s been a long time coming!), we’ll installGuard 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! I’ve used it extensively and it’s just beautiful to useand I’ve learned massively by making small contributions to the development. Learn more here https://github.com/discourse/discourse/

Page 27: EmberJS Testing on Rails

Chapter Two - Testing Setup 23

Initalize Guard

1 guard init rspec teaspoon

We’ll update the Guardfile that gets generated to take advantage of what spring has to offer.

Guardfile

1 guard :rspec, cmd: 'spring rspec' do

2 //.......

3 guard :teaspoon, cmd: 'spring teaspoon' do

We should try Guard now and ensure all it’s working.

Trying Guard

1 guard

Assuming all is well you should see a Guard running RSpec and the three suites defined withTeaspoon. There should be 0 examples and 0 failures at this stage. If not, it’s time to backtrackand find out where the issue is.

If all was well, let’s commit our changes now.

Commit changes

1 git add . -A

2 git commit -m 'Teaspoon and Guard'

Page 28: EmberJS Testing on Rails

Chapter Three - Routing SpecsOne of the things that Ember gets so right is the assertion that the URL should be the source of truth.Tom Dale gave a great talk about how front end development has a habit of ‘breaking’ the web, inparticular around the back button¹².

Getting the concept of routes correct in your mind is part of the learning curve with Ember and theway the map is somewhat different to Rails and can break your brain a bit when getting started!

Ember determines where to look for things based on the URL, as specified in the Router.

A Router was defined for us automatically when we bootstrapped Ember and it looks like this

app/assets/javascripts/router.js

1 AddressBook.Router.map(function() { });

At the moment this doesn’t seem very impressive, but we will soon discover we actually got quitea lot from that small declaration.

Let’s go ahead and see what we can discover about the router by writing a spec.

QUnit Specs (or Are They Tests?)

The convention with QUnit tends to be calling the files something_test.js, Teaspoon, however,expects something_spec.js, so we’ll use that convention.

A spec is made up of a module declaration in which we can declare what is required for setup andwhat to do after the specs have run. Let’s create a routing spec.

New file - spec/javascripts/unit/routing/router_spec.js

1 module('Routing specs', {

2 setup: function () {

3 AddressBook.reset();

4 }

5 });

¹²Stop breaking the web http://2013.jsconf.eu/speakers/tom-dale-stop-breaking-the-web.html

Page 29: EmberJS Testing on Rails

Chapter Three - Routing Specs 25

At the moment, this is mainly a placeholder, but the reset() call on our app in the setup is animportant step which will be called before each test is run - it ensures that our app is returned toit’s initial state. I prefer to put this in the setup, rather than teardown as it makes for easier visualdebugging when running test in the browser as it leaves the output on screen. When you focus ona 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 anonymousfunction containing the actual test.

spec/javascripts/unit/routing/router_spec.js

1 test('root route', function () {

2 visit('/');

3 andThen(function () {

4 var current_route = AddressBook.__container__.lookup('controller:applicat\

5 ion').currentRouteName;

6 equal(current_route, '', 'Expected ****** got: ' + current_route);

7 });

8 });

If you run this spec now, you’ll see we have a failing test!

Spec output

1 Failures:

2

3 1) Routing specs root route (1, 0, 1)

4 Failure/Error: Expected ****** got: index

Let’s commit those changes and then we’ll dig into what we’ve just discovered.

Commit changes

1 git add .

2 git commit -m 'First failing spec'

Page 30: EmberJS Testing on Rails

Chapter Three - Routing Specs 26

What’s going on here?

We’ve been introduced to some new methods here.

Name Parameters Explanation

visit(url) URL as string Visits the specified URL andreturn a promise ¹³

andThen(func) Anonymous function Ensures that any unfilledpromises succeed or fails thenexecutes the code in theanonymous function

equal(thingUnderTest,

expectedResult, optional

error message)

thingUnderTest=Object, string,anything really!, expectedResult=speaks for itself, optional errormessage can be a string, avariable or a combination

Tests for equality ofthingUnderTest andexpectedResult

So what we’ve done is visited the root route for our app and when the promise has been returnedwe’ve started digging about in Ember’s innards!

We set a variable current_route for use in our test. Whilst setting a variable isn’t strictly necessaryit gives us a nice construct for reporting on the outcome of the test.

..

Breaking it downAddressBook = the name of our application

__container__ = an Ember namespace construct. NEVER use this in production, only use fortesting and exploring

lookup('controller:application') = find our ApplicationController, note that the wording isreversed - controller, then application

currentRouteName = returns the name of the current route. This may be surprising to you if you’refamiliar with rails routes, but more on that later.

Now in our failing test we see that currentRoute is index. This top level route is created for usautomatically by Ember. We now know how to make our test pass and can update it to provide amore useful error message.

¹³Martin Fowler explains promises very simply here http://martinfowler.com/bliki/JavascriptPromise.html. Chris Webb provides a much moredetailed explanation here http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt1-theory-and-semantics

Page 31: EmberJS Testing on Rails

Chapter Three - Routing Specs 27

spec/javascripts/unit/routing/router_spec.js

1 test('root route', function () {

2 visit('/');

3 andThen(function () {

4 var current_route = AddressBook.__container__.lookup('controller:applicat\

5 ion').currentRouteName;

6 equal(current_route, 'index', 'Expected index got: ' + current_route);

7 })

8 });

Now clearly this wasn’t exactly a test first approach, but with this bit of exploration we know howto lookup URL’s and see how they are resolved.

Commiting changes

1 git add .

2 git commit -m 'Routing spec to green'

Creating a Contacts Route

Knowing what we do now, let’s test drive creating a route for a collection of contacts.

spec/javascripts/unit/routing/router_spec.js

1 test('contacts route', function () {

2 visit('/contacts')

3 andThen(function () {

4 var current_route = AddressBook.__container__.lookup('controller:applicat\

5 ion').currentRouteName;

6 equal(current_route, 'contacts', 'Expected contacts got:' + current_route\

7 );

8 });

9

10 });

We’ll now have the following failure:

Page 32: EmberJS Testing on Rails

Chapter Three - Routing Specs 28

Routing failure

1 Failures:

2

3 1) Routing specs contacts route (1, 1, 2)

4 Failure/Error: Error: Assertion Failed: The URL '/contacts' did not match an\

5 y routes in your application

6

7 Finished in 0.10100 seconds

8 2 examples, 1 failure

Great, we now have failing spec and we now know that we need to define a contacts route.

app/assets/javascripts/router.js

1 AddressBook.Router.map(function() {

2 this.resource('contacts');

3 });

And with that we should now have a passing spec! That was very straightforward and is a testamentto the convention over configuration approach.

Commiting changes

1 git add .

2 git commit -m 'Create a contacts route in Ember'

And So a Problem Presents Itself

With the contacts route in place we’ll create a template to render.

Handlebars templates should be placed in the app/assets/javascripts/templates folder and endwith either .hbs or .handlebars, i prefer the shorter version! The templates are pre-compiled intojavascript files and made available to your app.

We’ll create a test for what we expect to see in our template now. Let’s create a new file for ourintegration spec.

Page 33: EmberJS Testing on Rails

Chapter Three - Routing Specs 29

New file - spec/javascripts/integration/contacts_integrations_spec.js

1 module('Contacts integration', {

2 setup: function () {},

3 teardown: function () {

4 AddressBook.reset();

5 }

6 });

7

8 test('Contacts index page', function () {

9 visit('/contacts');

10 andThen(function () {

11 var header_text = find('.contacts_heading').text();

12 equal(header_text, "Now in the Contacts Index", 'Expected "Now in the Con\

13 tacts Index", got: ' + header_text );

14 });

15 });

We’re introduced to a new helper here find(). The find helper takes a string which explains whatwe’re looking for. It’s usage is essentially the same as the jQuery find() API¹⁴ and allows us tocreate complex lookups by nesting searches. For now though we’re simply going to look for a classcalled ‘contacts_heading’.

app/assets/javascripts/templates/contacts/index.hbs

1 <h1 class="contacts_heading">Now in the Contacts Index</h1>

Sowe should have a passing test now, right? Sadly not. So, what’s wrong?Well it turns out that whenwe specified the route earlier, what we got was a route that looks for contacts.hbs. Now it wouldn’tbe very sensible to keep all of our templates in one directory, so I created it in the contacts directoryand called it index.hbs. With the pre-compilation process it exists in Ember as contacts.index. Wedon’t 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! Bearwith me though and you’ll see why.

In order to fix this, we need to adjust the router.

¹⁴Find the full list of valid jQuery selectors here http://api.jquery.com/category/selectors/

Page 34: EmberJS Testing on Rails

Chapter Three - Routing Specs 30

app/assets/javascripts/router.js

1 AddressBook.Router.map(function () {

2 this.resource('contacts', function () {

3 });

4 });

We pass in an anonymous function and our integration spec goes green. Uh Oh, now our router specis red!

Router spec error

1 Failures:

2

3 1) Routing specs contacts route (1, 0, 1)

4 Failure/Error: Expected contacts got: contacts.index

Perfect, that error tells us exactly what went wrong and shows the value of taking the time to specifyverbose errors. So we have now learned that this.resource('contacts') creates a simple routecalled contacts, but when passed an anonymous function the route disappears and is replaced bycontacts.index. Essentially what’s happened here is that Ember has assumed that when we passedthe anonymous function we plan to create a bunch of other nested routes. Clever Ember!

Let’s update our spec to reflect reality now.

spec/javascripts/unit/routing/router_spec.js

1 equal(current_route, 'contacts.index', 'Expected contacts got:' + current_route);

OK great, we’ve now got a some routing specs and an integration spec, let’s commit our changes.

Commiting changes

1 git add .

2 git commit -m 'First integration spec'

An Abstraction Trying to Get Out?

Observant programmers amongst you would have noticed that our routing spec gets a bit repetitiveand we’re going to use the pattern repeatedly, so let’s do some light refactoring and create our owntest helper.

Page 35: EmberJS Testing on Rails

Chapter Three - Routing Specs 31

spec/javascripts/support/testing_helpers.js

1 var routesTo = function (url, route_name) {

2 visit(url);

3 andThen(function () {

4 var current_route = AddressBook.__container__.lookup('controller:applicat\

5 ion').currentRouteName;

6 equal(current_route, route_name, 'Expected ' + route_name + ', got: ' + c\

7 urrent_route);

8 });

9 };

This provides us with a really terse way of testing routes, tell it were we want to go and what routewe expect to see. Let’s add it to the spec_helper.js

spec/javascripts/spec_helper.js

1 //= require support/testing_helpers

We can now refactor our router_spec

spec/javascripts/unit/routing/router_spec.js

1 test('root route', function () {

2 routesTo('/', 'index');

3 });

4

5 test('contacts route', function () {

6 routesTo('/contacts', 'contacts.index');

7 });

Lovely, doesn’t that nice clean tidy code make you feel all warm inside??

Let’s commit our changes and then we’ll move on to putting some actual data on the page.

Commit changes

1 git add .

2 git commit -m ''

Page 36: EmberJS Testing on Rails

Chapter Four - Getting Started WithData RenderingAny exploration of Ember would be a total waste of time if we didn’t use the data bindings! Databindings allow us to iterate over data, update it, and if we desire, persist it to a data store. Withbindings we only need to update data in one place, all other references to the data are then updated.

In order to do this we’ll now explore Ember Models and routing to individual records.

Let’s get started by writing a failing spec.

spec/javascripts/integration/contacts_integration_spec.js

1 test('Renders contacts', function () {

2 visit('/contacts');

3 andThen(function () {

4 var contacts_length = find('.contacts_list li').length;

5 equal(contacts_length, 2, "Expected contacts to contain 2 items, got: " +\

6 contacts_length);

7 });

8 });

Great, now we’ve got a failing spec, let’s get on and get things working.

The first thing we will need in order to get this spec passing is a Route. Didn’t we already createa route I hear you ask? Well yes we did, but that was for routing not a route! Basically, whenyou declare a route in the Router an Ember.Route is created for you automatically. The Route forcontacts.index is called ContactsIndexRoute, this is great if we don’t 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 AddressBook.ContactsIndexRoute = Ember.Route.extend({

2 model: function () {

3 return this.store.findAll('contact'); // Note that contact is singular

4 }

5 });

What we’ve done here is said that when we visit the contacts index, go to the application data storeand find all records for the contact data model and attach the result to a model property.

Page 37: EmberJS Testing on Rails

Chapter Four - Getting Started With Data Rendering 33

If we try to run our spec now we’ll 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 theactual spec output.

Error - truncated

1 Error while loading route: Error: No model was found for 'contact'

2 at http://127.0.0.1:57169/assets/ember-data.js?body=1:3027

3 at http://127.0.0.1:57169/assets/ember-data.js?body=1:2629

4 at http://127.0.0.1:57169/assets/routes/contacts/contacts_index_route.js?body\

5 =1:3

Models

So we now know that there is no contact model available, so let’s declare a model for storing ourcontact information.

New file - app/assets/javascripts/models/contacts.js

1 AddressBook.Contact = DS.Model.extend({

2 first_name: DS.attr('string'),

3 last_name: DS.attr('string')

4 });

We can, if we choose simply declare DS.attr without specifying the type but, my preference is tobe 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 what’s going on? When wevisit a route and ask for the model, Ember automatically makes a request to the server for the data.In our case it’s going to hit http://localhost:3000/contacts. Anyone familiar with Rails willquickly recognise that we haven’t created any of the Rails infrastructure necessary to support thiscall so it responds with a 404 (not found) error. This failure results model call in the route to returnundefined and stalls the application.

It’s not very useful to have the test suite hitting the server for data all the time anyway, so let’sensure that we can get some data internally.

Page 38: EmberJS Testing on Rails

Chapter Four - Getting Started With Data Rendering 34

spec/javascripts/integration/contacts_integration_spec.js

1 module('Contacts integration', {

2 setup: function () {

3 AddressBook.ApplicationAdapter = DS.FixtureAdapter;

4 },

5 teardown: function () {

6 AddressBook.reset();

7 }

8 });

The Ember Fixture Adapter allows us to have pre specified data to use in our tests. This makes itvery easy for us to know what to test for.

Fixture error

1 Error while loading route: Error: Assertion Failed: Unable to find fixtures for m\

2 odel type AddressBook.Contact

3 at http://127.0.0.1:57169/assets/ember.js?body=1:81

4 at http://127.0.0.1:57169/assets/ember-data.js?body=1:8109

5 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 specifiedas a javascript Array of Objects.

spec/javascripts/integration/contacts_integration_spec.js

1 module('Contacts integration', {

2 setup: function () {

3 AddressBook.ApplicationAdapter = DS.FixtureAdapter;

4 AddressBook.Contact.FIXTURES = [

5 {

6 id: 1,

7 first_name: 'Dave',

8 last_name: 'Crack'

9 },

10 {

11 id: 2,

12 first_name: 'Dustin',

13 last_name: 'Hoffman'

14 }

15 ]

Page 39: EmberJS Testing on Rails

Chapter Four - Getting Started With Data Rendering 35

16 },

17 teardown: function () {

18 AddressBook.reset();

19 }

20 });

Great, we’ve finally got a useful error, and if you’re wondering about Dustin Hoffman and DaveCrack, see the footnote!¹⁵

Error message

1 1) Contacts integration Renders contacts (1, 0, 1)

2 Failure/Error: Expected contacts to contain 2 items, got: 0

We can now update our index template to render the data we’ve just setup.

app/assets/javascripts/templates/contacts/index.hbs

1 <h1> .... </h1>

2 <ul class="contacts_list">

3 {{#each}}

4 <li>First Name: {{first_name}}</li>

5 {{/each}}

6 </ul>

Here we create an unordered list and using the {{#each}}{{/each}} helper, asking Ember to createa <li> element for each item in the array. Inside the iterator we ask for the first_name property tobe rendered. The first name is received from the model

With that we should now have a passing spec.

Commit time again!

Commit changes

1 git add .

2 git commit -m 'Contacts integration spec to green'

Unit Testing Our Model

When I’m working with Rails I really like to use the RSpec construct for testing models:

¹⁵Dave Crack is a London cab driver who was given mention by Dustin Hoffman as part of a BAFTA acceptance speech. I love the story becauseit’s a great example of humility. http://www.zipadeeday.com/story/60/uk-dinner-lady-flabbergasted-by-hoffman-call/

Page 40: EmberJS Testing on Rails

Chapter Four - Getting Started With Data Rendering 36

RSpec example

1 subject { Contact.new }

2 it {should respond_to :some_property_name }

3 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 dosomething similar with Ember.

New file - spec/javascripts/unit/models/contacts_spec.js

1 module('Contacts Model', {

2 setup: function () {},

3 teardown: function () {

4 AddressBook.reset();

5 }

6 });

7

8 test('Has a first_name property', function () {

9 var first_name = AddressBook.Contact.metaForProperty('first_name');

10 equal(first_name.type, 'string');

11 ok(first_name.isAttribute);

12 });

Whenwe create aModel in Ember it is attached to the top level namespace, hence AddressBook.Contact.With the metaForProperty('first_name') call we receive an object withmetadata 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 lightmetaprogramming! We’ll also introduce the ok() test helper. ok() takes an argument that shouldevaluate as a boolean and an optional second argument that is the error message.

spec/javascripts/support/testing_helpers.js

1 var respondsTo = function (model, attribute, type) {

2 var test_subject = AddressBook[model].metaForProperty(attribute);

3 equal(test_subject.type, type, 'Expected ' + type + " got: " + test_subject.t\

4 ype);

5 ok(test_subject.isAttribute);

6 };

We can then refactor our original to use the new helper and check the other property.

Page 41: EmberJS Testing on Rails

Chapter Four - Getting Started With Data Rendering 37

spec/javascripts/unit/models/contacts_spec.js

1 test('attributes', function () {

2 respondsTo('Contact', 'first_name', 'string');

3 respondsTo('Contact', 'last_name', 'string');

4 });

Now there are many argument for and against this pattern in testing. Many would argue that it’sbrittle 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 we’re lookingfor before we implement it gives us great insight into what’s going on internally. Later when weencounter 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 didn’t test the Model first in this chapter, but we will in future.

We’ll commit our changes now and in the next chapter we’ll look at nested routes.

Committing changes

1 git add .

2 git commit -m 'Model testing and helper'

An Unexpected Problem

In defining the ContactsIndexRoute we’ve 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 reportingFailure/Error: Expected contacts.index, got: index. The reason for this is that now wehave declared the ContactsIndexRoute the first thing we ask it to do is to retrieve all contactsfrom the server. We don’t have anything setup in Rails at this stage so the server returns a404 - Not Found error, which means we can’t transition to contacts.index. This happens if wetry to move from ‘/’ to ‘/contacts’. If we try to hit ‘/contacts’ without first hitting ‘/’ we geta 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 sothat the real server is never hit during testing.

Let’s do that now.

Page 42: EmberJS Testing on Rails

Chapter Four - Getting Started With Data Rendering 38

spec/javascripts/spec_helper.js

1 AddressBook.ApplicationAdapter = DS.FixtureAdapter;

2 AddressBook.Contact.FIXTURES = [

3 {

4 id: 1,

5 first_name: 'Dave',

6 last_name: 'Crack'

7 },

8 {

9 id: 2,

10 first_name: 'Dustin',

11 last_name: 'Hoffman'

12 }

13 ];

We can now remove this from the contacts_integration_spec.js.

Commit changes

1 git add .

2 git commit -m 'Refactor FixtureAdapter'

Page 43: EmberJS Testing on Rails

Chapter Five - Nested RoutesSo far we’ve worked with only index level routes, but what if we want to look at an individual item?

Well there are a number of ways we can do this and as yet, there hasn’t been a ‘golden path’ agreedby the Ember team., so this section may change in the future.¹⁶

As we saw earlier in our router we can use the this.resource construct, which allows us to nestroutes into logical groupings. Let’s consider what we need for a moment.

Name What we want

contacts A listing of all our contactscontacts/:contact_id A view of a single contactcontacts/new A form to create a new contact

With this in mind we can now define our nested routes and I will use the Rails style of index, show,new etc.

Let’s start with a failing spec.

spec/javascripts/unit/routing/router_spec.js

1 //....

2 test('individual contact', function () {

3 routesTo('/contacts/1', 'contacts.show');

4 });

This should give us an error similar to this:

Error message

1 1) Routing specs individual contact (2, 0, 2)

2 Failure/Error: Error: Assertion Failed: The URL '/contacts/1' did not match \

3 any routes in your application

4

5 2) Routing specs individual contact (2, 0, 2)

6 Failure/Error: TypeError: 'undefined' is not an object (evaluating 'AddressB\

7 ook.__container__.lookup('controller:application').currentRouteName')

Great, no route matches, so let’s define one by updating our router.

¹⁶A fuller discussion on the topic is available here http://emberjs.com/guides/routing/defining-your-routes/, with some supplementary opinionshere http://hashrocket.com/blog/posts/ember-routing-the-when-and-why-of-nesting

Page 44: EmberJS Testing on Rails

Chapter Five - Nested Routes 40

app/assets/javascripts/router.js

1 AddressBook.Router.map(function () {

2 this.resource('contacts', function () {

3 this.route('show', {path: '/:contact_id'});

4 });

5 });

Our spec should now be green. Let’s consider what we’ve just done in a bit more detail.

Defining Routes Within Resources

Construct Use case Notes

this.resource('name',

function () {})

Creates a namespaced route inwhich other routes can be nested

Ember guide recommends usingresource for URL’s that representa noun or a class of thing e.g.Contacts, Posts

this.route('names', {path:

'/:segment_id'})

Define a visitable route and canbe created top level or nestedwithin a resource

The path option allows us to takein a parameter by using :

In our example we get a route called contacts.show which takes the final URL segment as aparameter which gets bound to contact_id which we can use in our controller for looking up therecord.

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 AddressBook.ContactsShowRoute = Ember.Route.extend({

2 model: function (params) {

3 return this.store.find('contact', params.contact_id);

4 }

5 });

The main difference here between our ContactsShowRoute and the the ContactsIndexRoute is thatthe model only asks for one contact and does so via the params.contact_id. contact_id got definedin our router via {path: '/:contact_id'}.

Page 45: EmberJS Testing on Rails

Chapter Five - Nested Routes 41

Rendering the Individual Contact

If we want to render the individual contact we will need a template, so we will write a failingintegration test.

spec/javascripts/integration/contacts_integration_spec.js

1 //....

2 test('Renders only one contact', function () {

3 visit('/contacts/1');

4 andThen(function () {

5 var contact = find('#contact h1').text();

6 var expected_result = 'Details for Contact 1';

7 equal(contact, expected_result, 'Expected: ' + expected_result + ' got: '\

8 + contact);

9 });

10 });

With this test we will visit the newly defined route and the use the find helper to find an htmlelement with the id of contact and collect the text. We expect that this text is “Details for Contact1”.

We can now create the template.

New file - app/assets/javascripts/templates/contacts/show.hbs

1 <div id="contact">

2 <h1>Details for Contact {{id}}</h1>

3 <ul>

4 <li>First Name: {{first_name}}</li>

5 <li>Last Name: {{last_name}}</li>

6 </ul>

7 </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

Let’s 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 removesduplication and ensures that we don’t forget to put it in anywhere.

Page 46: EmberJS Testing on Rails

Chapter Five - Nested Routes 42

spec/javascripts/spec_helper.js

1 AddressBook.injectTestHelpers();

2 # ....

3

4 QUnit.testStart = function () {

5 AddressBook.reset();

6 };

The change of reseting the app before each test rather than afterwards gives us an opportunity tohave a look at the application state in the browser. Let’s see how that is done.

An Alternate Way of Viewing Our Specs

So far we’ve only relied on the command line interface for viewing the outcome of our specs, butwhat does our app actually look like?? Teaspoon comes with a very pretty web interface for runningthe suite.We can view it view the standard rails server by visiting http://localhost:3000/teaspoon.Have a look now and you should see something like this.

Teaspoon web interface

This is kinda neat! The CSS we set up earlier is a trick that allows us to render the results of the testsin 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.

Page 47: EmberJS Testing on Rails

Chapter Five - Nested Routes 43

This is particularly useful because wewill see the results of a failing spec here and can really visualisewhat’s going on. I prefer to relymostly on the command line approach andmove to the web interfaceif I just can’t figure out why things are going wrong.

Navigating Around the App

Knowing that we can directly visit contact number 1 is nice, but wouldn’t it be better if our userscould click on a name in their index and move to the details page?

Let’s create a spec for that!

spec/javascripts/integration/contacts_integration_spec.js

1 //..

2 test('Visiting a contact via the index screen', function () {

3 visit('/contacts').click('ul li:last a');

4 andThen(function () {

5 var contact = find('#contact h1').text();

6 var expected_result = 'Details for Contact 2';

7 equal(contact, expected_result, 'Expected: ' + expected_result + ' got: '\

8 + contact);

9 });

10 });

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 inthe list and not the first.

find ul -> then find last li element -> then find the a tag -> then click it!

Once we’ve 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 todetect any issues I may have not picked up otherwise.

With that, we should have a failing spec similar to this

Page 48: EmberJS Testing on Rails

Chapter Five - Nested Routes 44

Failure

1 1) Contacts integration Visiting a contact via the index screen (1, 0, 1)

2 Failure/Error: Error: Element ul li:last a not found.

We can use the link-to helper now to get everything working.

/app/assets/javascripts/templates/contacts/index.hbs

1 <ul class="contacts_list">

2 {{#each}}

3 <li>{{#link-to 'contacts.show' this}}{{first_name}}{{/link-to}}</li>

4 {{/each}}

5 </ul>

With this helper we are asking Ember to go to the contact.show route which is passed as a stringand 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, we’re back to green.

Time for a commit before we switch things over to Rails to get an idea about the infrastructureneeded to allow persistence in our app.

Commiting changes

1 git add .

2 git commit -m 'Individual contacts and specs'

Page 49: EmberJS Testing on Rails

Chapter Six - Persisting Data WithRailsThere’s been a lot of controversy with Ember Data. Yehuda himself discussed the problems withtheir initial implementation of Ember Data.¹⁷ Most of the issues there have been ironed out now asa result of a major rewrite. It’s looking very good these days and provides a lot of functionality. Wewill use this later, but for now we will focus on getting the backend set up.

Setting Up the Backend

We’ve managed to get to Chapter 6 before writing any real Rails code! We are going to go throughthis section pretty quickly because the focus of the book is on Ember, not Rails.

I personally avoid using generators wherever possible as I’ve alluded to earlier, but for the purposewe have here they will serve us well.

Generating Contact Model

1 rails g model Contact first_name:string last_name:string

We should get a few files generated

Files generated

1 invoke active_record

2 create db/migrate/20140204004246_create_contacts.rb

3 create app/models/contact.rb

4 invoke rspec

5 create spec/models/contact_spec.rb

6 invoke factory_girl

7 create spec/factories/contacts.rb

We’ll update the spec now.

¹⁷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

Page 50: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 46

spec/models/contact_spec.rb

1 require 'spec_helper'

2

3 describe Contact do

4 let(:contact) { FactoryGirl.build_stubbed(:contact) }

5

6 subject { contact }

7

8 it { should respond_to :first_name }

9 it { should respond_to :last_name }

10 end

In this test we use the #build_stubbed method. I particularly like this pattern because it generatesan instance of the model, but without any persistence. This is great because it keeps our tests reallyfast because the database doesn’t get hit. We can already feel confident that the persistence layer ofRails is very well tested, so there’s no need to double up.

We’re testing here that our model has first_name and last_name properties. If we run the spec nowwe’ll get a failure because we haven’t migrated the database.

Let’s fix that.

Migrate

1 rake db:migrate && rake db:test:prepare

We’ll 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.

Page 51: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 47

Contacts failing spec

1 spec/controllers/api/v1/contacts_controller_spec.rb:3:in `<top (required)>': unin\

2 itialized constant Api::V1::ContactsController (NameError)

This is easily resolved.

New file - app/controllers/api/v1/contacts_controller.rb

1 class Api::V1::ContactsController < ApplicationController

2

3 end

Great, so now we want to return JSON representing all of our contacts.

spec/controllers/api/v1/contacts_controller_spec.rb

1 # ...

2

3 describe Api::V1::ContactsController do

4 describe 'GET methods' do

5 it 'index' do

6 @contacts = FactoryGirl.create_list(:contact, 2)

7 get :index

8 end

9 end

10 end

We ask FactoryGirl to create (and persist) two contacts so we have some data to work with and totry to GET the index method on our controller. We’ll get a failing spec similar to this

Failing spec

1 1) Api::V1::ContactsController GET methods index

2 Failure/Error: get :index

3 ActionController::UrlGenerationError:

4 No route matches {:action=>"index", :controller=>"api/v1/contacts"}

We can define the route now.

Page 52: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 48

config/routes.rb

1 # ......

2

3 get 'assets/index'

4 namespace :api do

5 namespace :v1 do

6 resources :contacts

7 end

8 end

And create an index method in our controller.

app/controllers/api/v1/contacts_controller.rb

1 class Api::V1::ContactsController < ApplicationController

2 def index

3 end

4 end

Our error will be similar to this.

Failing spec

1 1) Api::V1::ContactsController GET methods index

2 Failure/Error: get :index

3 ActionView::MissingTemplate:

4 Missing template api/v1/contacts/index, application/index with {:locale=>[\

5 :en], :formats=>[:html], :handlers=>[:erb, :builder, :raw, :ruby, :jbuilder]}. Se\

6 arched in:

7 * "#<RSpec::Rails::ViewRendering::EmptyTemplatePathSetDecorator:0x007fe1\

8 fa991ce8>"

This is one of my personal frustrations with controller testing in Rails - the errors you get when aview isn’t defined. There are twoways we can fix this, we can create views/api/v1/index.html.erbas an empty file or we can update our controller to render nothing. I prefer the second approachbecause we are not actually going to be creating standard views.

Page 53: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 49

/app/controllers/api/v1/contacts_controller.rb

1 def index

2 render json: nil

3 end

Our specs should now be passing. Simply rendering nothing isn’t particularly useful though, so nextlets expect some data.

spec/controllers/api/v1/contacts_controller_spec.rb

1 get :index

2 assigns(:contacts).length.should == 2

Here we are expecting an instance variable called contacts with a length of 2.

The simplest step to get our spec to green is this.

/app/controllers/api/v1/contacts_controller.rb

1 def index

2 @contacts = [1,2]

3 render json: @contacts

4 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 get :index

2 assigns(:contacts).length.should == 2

3 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.

Page 54: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 50

/app/controllers/api/v1/contacts_controller.rb

1 def index

2 @contacts = Contact.all

3 render json: @contacts

4 end

Beautiful, specs are green again.

Time to commit again.

Commiting changes

1 git add .

2 git commit -m 'Build Rails API for Contacts'

Serializing the Data

In order to pass data to Ember, it is expected in a particular format. Luckily ActiveModelSerializeris included with Ember-Rails and makes this pretty trivial.

New file - /app/serializers/contact_serializer.rb

1 class ContactSerializer < ActiveModel::Serializer

2 attributes :id, :first_name, :last_name

3 end

We define a serialiser which intercepts our @contacts from the controller and adjusts the outputformat 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 yourmind around it.

Page 55: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 51

Before ActiveModelSerializer is created

1 {"contacts":[{"contacts":{"id":1,"first_name":"Dave","last_name":"Crack","created\

2 _at":"2014-02-04T02:16:38.491Z","updated_at":"2014-02-04T02:16:38.491Z"}},{"conta\

3 cts":{"id":2,"first_name":"Dustin","last_name":"Hoffman","created_at":"2014-02-04\

4 T02:16:54.229Z","updated_at":"2014-02-04T02:16:54.229Z"}}]}

After creation

1 {"contacts":[{"id":1,"first_name":"Dave","last_name":"Crack"},{"id":2,"first_name\

2 ":"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

This will give us an interactive console. Enter the following.

Entering data in the console

1 Contact.create(first_name: 'Dave', last_name: 'Crack')

2 Contact.create(first_name: 'Dustin', last_name: 'Hoffman')

3 exit

Great, we’re now ready to connect.

Connecting the Backend to the Frontend!

We’ve done a lot of infrastructure building, but nothing much in the way of actually looking at ourapp! This has been very deliberate because I want to emphasise the test driving approach. We arenow in a position to link our fronted and backend in just a couple of lines of code.

Page 56: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 52

/app/assets/javascripts/store.js

1 AddressBook.Store = DS.Store.extend({

2 adapter: '-active-model'

3 });

4 DS.RESTAdapter.reopen(

5 {namespace: "api/v1"}

6 );

Here we’re telling Ember to use the DS.ActiveModelSerializer for our data. We also tell theDS.RESTAdapter that all of our JSON requests should be prefixed with api/v1. In this way shouldwe 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 git add .

2 git commit -m 'Connecting the frontend to the backend'

More Routes Necessary

All is working well in our fronted for now, but if we visit localhost:3000/#/contacts/1 withoutfirst hitting the index, we’re going to have a problem. The reason for this is that we don’t have aRails controller action to return a single Contact only a collection.

Failing spec time again!

spec/controllers/api/v1/contacts_controller_spec.rb

1 describe 'GET methods' do

2 #......

3 it 'show' do

4 @contact = FactoryGirl.create(:contact)

5 get :show, id: @contact.id

6 assigns(:contact).first_name.should == 'MyString'

7 end

8 end

We can now add our show method to the controller

Page 57: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 53

/app/controllers/api/v1/contacts_controller.rb

1 #..

2 def show

3 @contact = Contact.find(params[:id])

4 render json: @contact

5 end

Specs to green, but let’s refactor our controller and take advantage of StrongParameters.¹⁸

/app/controllers/api/v1/contacts_controller.rb

1 class Api::V1::ContactsController < ApplicationController

2 before_action :set_contact, only: [:show]

3 def index

4 render json: @contacts = Contact.all

5 end

6

7 def show

8 render json: @contact

9 end

10

11 private

12

13 def set_contact

14 @contact = Contact.find(params[:id])

15 end

16

17 end

Awesome, that’s nice and neat and in keeping with the patterns promoted by StrongParameters.

Commit changes

1 git add .

2 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

Page 58: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 54

Adding Contacts

So far we’ve only dealt with data we’ve pushed directly into the store, we will now look at how toadd a contact of our own.

A reasonable pattern for this to have a button for adding which changes the view to present us withan input field, a save button and a cancel button.

Let’s start with an integration spec for showing an input field.

spec/javascripts/integration/contacts_integration_spec.js

1 //..

2 test('Show input for new contact', function () {

3 visit('/contacts').click('#add_new_contact');

4 andThen(function () {

5 var input_field = find('#new_first_name').length;

6 ok(input_field == 1, 'Input field not found');

7 });

8 });

So we expect that when we visit /contacts and click the button with id ‘add_new_contact’ then wewill see an input field with an id of ‘new_first_name’.

/app/assets/javascripts/templates/contacts/index.hbs

1 <h1 class="contacts_heading">Now in the Contacts Index</h1>

2 <button id="add_new_contact"></button>

3 <ul class="contacts_list">

And with that we have the following failure.

Failure message

1 Failures:

2

3 1) Contacts integration Show input for new contact (1, 0, 1)

4 Failure/Error: Input field not found

So how do we go about fixing this up? Firstly we will need to create a controller for our contactsroute which will give us a place to hang actions from.

Page 59: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 55

New file - /app/assets/javascripts/controllers/contacts_index_controller.js

1 AddressBook.ContactsIndexController = Ember.ArrayController.extend({

2 actions: {

3 addNewContact: function () {

4

5 }

6 }

7 });

ContactsIndexController means that this controller is associated with the ContactsIndexRoute

and will be available when we are visiting the /contact URL. We can defined custom attributes andfunctions in this object to add functionality. Defining an actions object allows us to bind eventssuch as click events to a specific action.

We will add the action to the template now and then define the action required to get the specpassing.

/app/assets/javascripts/templates/contacts/index.hbs:1

1 <h1 class="contacts_heading">Now in the Contacts Index</h1>

2 <button id="add_new_contact" {{action 'addNewContact'}}></button>

3 <ul class="contacts_list">

Our spec at this point will still be failing because although we have a button, clicking it doesn’t doanything just yet.

We should define an action to occur on click now.

/app/assets/javascripts/controllers/contacts_index_controller.js

1 actions: {

2 addNewContact: function () {

3 this.toggleProperty('addingNewContact');

4 }

We will now use a conditional in our template to show the input field based on whetheraddingNewContact is true or not.

Page 60: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 56

/app/assets/javascripts/templates/contacts/index.hbs

1 {{#if addingNewContact}}

2 <label for="new_first_name">First name</label>

3 {{input type='text' value=controller.new_first_name id='new_first_name'}}

4 {{else}}

5 <button id="add_new_contact" {{action 'addNewContact'}} >Add new contact</but\

6 ton>

7 {{/if}}

With this update we show the button if we are not editing and if we are, then we hide the button andshow a the new_first_name input field.We use the ember input helper to create a text input field withthe 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.

We’ll add a bit more now, because most people have a first and a last name! With that in mind, let’supdate the spec we wrote.

spec/javascripts/integration/contacts_integration_spec.js

1 //.....

2 andThen(function () {

3 var first_name_field = find('#new_first_name').length;

4 var last_name_field = find('#new_last_name').length;

5 ok(first_name_field == 1, 'First name field not found');

6 ok(last_name_field == 1, 'Last name field not found');

7 });

Now you know how to get this spec passing!

/app/assets/javascripts/templates/contacts/index.hbs

1 {{#if addingNewContact}}

2 <label for="new_first_name">First name</label>

3 {{input type='text' value=controller.new_first_name id='new_first_name'}}

4 <label for="new_last_name">Last name</label>

5 {{input type='text' value=controller.new_last_name id='new_last_name'}}

We should now handle the case for saving our contact.

Page 61: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 57

spec/javascripts/integration/contacts_integration_spec.js

1 //.....

2 test('Adding a new contact', function () {

3 visit('/contacts').click('#add_new_contact');

4 fillIn('#new_first_name', 'Buzz');

5 fillIn('#new_last_name', 'Lightyear');

6 click('#save_new_contact');

7 andThen(function () {

8 var first_name = find('.contacts_list:contains("Buzz")').length;

9 ok(first_name == 1, "First name was not saved");

10 });

11 });

Let’s get started with updating the template by adding the save button.

/app/assets/javascripts/templates/contacts/index.hbs

1 {{#if addingNewContact}}

2 //....

3 {{input type='text' value=controller.new_last_name id='new_last_name'}}

4 <button id="save_new_contact" {{action 'saveNewContact'}} >Save new contact</butt\

5 on>

Our failing spec is now tell us what to do.

Failing spec

1 Failures:

2

3 1) Contacts integration Adding a new contact (1, 0, 1)

4 Failure/Error: Error: Nothing handled the action 'saveNewContact'. If you di\

5 d handle the action, this error can be caused by returning true from an action ha\

6 ndler in a controller, causing the action to bubble.

We need to define the saveNewContact function.

Page 62: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 58

/app/assets/javascripts/controllers/contacts_index_controller.js

1 actions: {

2 //...

3 },

4 saveNewContact: function () {

5 var new_first_name = this.get('new_first_name');

6 var new_last_name = this.get('new_last_name');

7

8 var new_contact = this.store.createRecord('contact',{

9 first_name: new_first_name,

10 last_name: new_last_name

11 });

12

13 new_contact.save();

14 }

Again, our spec is green, but it would be great if the text fields were cleared and we were returnedto the initial view with an add button.

spec/javascripts/integration/contacts_integration_spec.js

1 //....

2 ok(first_name == 1, "First name was not saved");

3 var add_new_contact_button = find('#add_new_contact').length;

4 ok(add_new_contact_button == 1, "Have not transitioned back to original s\

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.Let’s use that to go back to the original view if we succeed and to show an alert if it fails.

Page 63: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 59

/app/assets/javascripts/controllers/contacts_index_controller.js

1 saveNewContact: function () {

2 //..

3 var self = this;

4 new_contact.save().then(

5 function ( {

6 self.set('new_first_name', '');

7 self.set('new_last_name', '');

8 self.toggleProperty('addingNewContact');

9 },

10 function () {alert('Unable to save record'); });

11 }

With the promise pattern it’s important to know that when the promise returns, thismayno longer be this, it may be another this! In order to prevent any issues we explicitlyset a reference to the current context by var self = this;. In the success portion of ourpromise, we call the .toggleProperty on self.

The two this.set calls, these aren’t strictly necessary, but I think it’s nice if when a user later clicksadd 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, we’llneed 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 describe Api::V1::ContactsController do

2 #......

3 describe 'POST methods' do

4 it 'creates a new contact' do

5 @contact = FactoryGirl.attributes_for(:contact)

6 expect{post :create, contact: @contact}.to change(Contact, :count).by(1)

7 end

8 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 theattributes as JSON. We wrap the call in an expect block and tell it we that Contact.count shouldchange by 1.

Page 64: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 60

/app/controllers/api/v1/contacts_controller.rb

1 #.....

2 def create

3 @contact = Contact.new(get_contact_params)

4 if @contact.save

5 render json: @contact

6 else

7 render json: @contact.errors, status: :unprocessable_entity

8 end

9 end

10

11 private

12 #....

13 def get_contact_params

14 params.require(:contact).permit([:first_name, :last_name])

15 end

For those not familiar with the StrongParameters pattern, in get_contact_params we state that werequire the POST request to have a contact, and that on that contact we will permit a first_nameand a last_name. Anything else will be discarded. This pattern helps us to keep malicious code outof our system. Perhaps, for example a user model has an admin property, if we don’t protect this amalicious user could send a request to set the admin flag to true, giving them unauthorized privilegeson the site. With StrongParams, an unauthorized admin flag would be discarded.

And with this our specs should again be green.

Let’s commit

Commit changes

1 git add.

2 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 tocancel creation of a contact and deleting an existing contact.

We’ll start with cancelling.

Page 65: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 61

spec/javascripts/integration/contacts_integration_spec.js

1 test('Cancelling creation of new contact', function () {

2 visit('/contacts').click('#add_new_contact');

3 andThen(function () {

4 var first_name_field = find('#new_first_name').length;

5 ok(first_name_field == 1, 'First name field not found');

6 click('#cancel_new_contact');

7 andThen(function () {

8 var add_new_contact_button = find('#add_new_contact').length;

9 ok(add_new_contact_button == 1, "Have not transitioned back to origin\

10 al state");

11 });

12 });

13 });

Here we transition to the add_new_contact view, check that we are there. We then click the cancelbutton and check we have return to the original view. As can be see here, we can nest the andThencalls.

We should update the template now.

/app/assets/javascripts/templates/contacts/index.hbs

1 <!-- =-->

2 <button id="save_new_contact" {{action 'saveNewContact'}} >Save new contact</butt\

3 on>

4 <button id="cancel_new_contact"{{action 'cancelNewContact'}}>Cancel new contact</\

5 button>

This pattern should look pretty familiar now. Our spec will now complain that Nothing handled

the action 'cancelNewContact', and i’m sure you know now how to fix that up.

/app/assets/javascripts/controllers/contacts_index_controller.js

1 //.....

2 },

3 cancelNewContact: function () {

4 this.set('new_first_name', '');

5 this.set('new_last_name', '');

6 this.toggleProperty('addingNewContact');

7 }

Page 66: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 62

Deleting a Contact

The final thing we’ll 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 test('Deleting a contact', function () {

3 visit('/contacts').click('.contacts_list li:first .delete_button');

4 andThen(function () {

5 var contacts = find('ul li').length;

6 ok(contacts == 1, "Exepcted 1 contact got: " + contacts);

7 });

8 });

So we visit contacts, click on the first delete button and then expect our list of contacts to shrink to1.

/app/assets/javascripts/templates/contacts/index.hbs

1 {{#each}}

2 <li>

3 {{#link-to 'contacts.show' this}}{{first_name}}{{/link-to}}

4 <button {{action 'deleteContact' this}} class="delete_button">Delete \

5 contact</button>

6 </li>

7 {{/each}}

The only real difference with this call is that when we specify the deleteContact action we alsopass in this, which makes the current object available in the controller.

We can use the Ember Data’s destroyRecord function to remove the record from the store and senda delete request to the server.

Page 67: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 63

/app/assets/javascripts/controllers/contacts_index_controller.js

1 //....

2 },

3 deleteContact: function (contact) {

4 contact.destroyRecord();

5 }

contact is received from the template and we simply make the correct function call on it.

At this point you’ll notice that our spec is still failing and the reason is our FIXTURES don’t getreset between tests. When we added our Buzz Light year, we upped the number to 3, and with thedelete it’s down to 2. Grr!

In order to resolve this issue, it would be good to know that we start each integration spec with aclean data set. We’ll do a spot of refactoring now

spec/javascripts/spec_helper.js

1 var resetFixtures = function () {

2 AddressBook.Contact.FIXTURES = [

3 {

4 id: 1,

5 first_name: 'Dave',

6 last_name: 'Crack'

7 },

8 {

9 id: 2,

10 first_name: 'Dustin',

11 last_name: 'Hoffman'

12 }

13 ];

14 };

15

16 resetFixtures();

We are nowwrapping 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 inthe integration specs. Lets do that now.

Page 68: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 64

spec/javascripts/integration/contacts_integration_spec.js

1 teardown: function () {

2 resetFixtures();

3 }

Great, all green again.

We will of course now need to define a #delete method in rails.

spec/controllers/api/v1/contacts_controller_spec.rb

1 describe Api::V1::ContactsController do

2 #..

3 describe 'DELETE method' do

4 it 'deletes a contact' do

5 @contact = FactoryGirl.create(:contact)

6 expect{delete :destroy, id: @contact.id}.to change(Contact, :count).by(-1)

7 end

8 end

In order to get this passing we’ll need to update our controller.

/app/controllers/api/v1/contacts_controller.rb

1 class Api::V1::ContactsController < ApplicationController

2 before_action :set_contact, only: [:show, :destroy]

3

4 #....

5

6 def destroy

7 if @contact.destroy

8 render json: nil

9 else

10 render json: @contact.errors, status: :unprocessable_entity

11 end

12 end

All is well on our specs, but we’ll still have a problem if we try to delete a contact in our real appand this is due to Cross Site Request Forgery protections that Rails adds by default. We will solvethis 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 providingyou with access to generators. https://github.com/dockyard/ember-appkit-rails/

Page 69: EmberJS Testing on Rails

Chapter Six - Persisting Data With Rails 65

/app/assets/javascripts/application.js.erb

1 //= require address_book

2 //= require csrf

/app/assets/javascripts/csrf.js

1 $.ajaxPrefilter(function(options, originalOptions, jqXHR) {

2 var token;

3 if (!options.crossDomain) {

4 token = $('meta[name="csrf-token"]').attr('content');

5 if (token) {

6 return jqXHR.setRequestHeader('X-CSRF-Token', token);

7 }

8 }

9 });

This takes the CSRF token from the page header and adds it to all of our ajax requests. And withthat, we should be able to add and delete contacts.

Time for a commit

Commit changes

1 git add .

2 git commit -m 'Add and remove contacts'

Page 70: EmberJS Testing on Rails

Chapter 7 - Relationships in EmberRelationships are hard, but not in Ember! Relationships allow us to describe how our various modelsrelate to each. This is a very common pattern in Rails with the has_many and belongs_to helpersbeing two of the types available.²⁰

There are fewer types available in Ember, but they are no less powerful when we put them to use.

We’ve got our basic contact set up with a first and last name, but it would be good to add emailaddresses and telephone numbers. Let’s start with email addresses.

There area number of things we’ll need to do in this section.

• Ember model for email addresses• Rails model for email addresses• Contact -> email relationship in Ember• Contact -> email relationship in Rails• A way of rendering the email addresses in the contact show template

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, we’ll create a newfile to keep our tests tidy.

New file - spec/javascripts/integration/email_integration_spec.js1 module('Email address integration');

2

3 test('Showing associated email addresses', function () {

4 visit('/contacts/1');

5 andThen(function () {

6 var emails = find('.email_address');

7 ok(emails.length == 2, "Expected two emails got: " + emails.length);

8 equal(emails[0].innerText, '[email protected]', 'Expected [email protected] got\

9 : ' + emails[0].innerText);

10 });

11 });

With this spec we’re looking for elements with the email_address class and checking to see howmany exist. We’re also checking to specify that the first element contains specific text. The reasonfor the two types of test will become apparent later.

Running our spec we’ll get a failure

²⁰For more information on relationships in Rails visit http://guides.rubyonrails.org/association_basics.html#the-types-of-associations

Page 71: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 67

Failure message

1 Failures:

2

3 1) Email address integration Showing associated email addresses (1, 0, 1)

4 Failure/Error: Expected two emails got: 0

At this stage we’ll live with this failing spec for a while as we move to implement the infrastructurerequired to get it to green.

A Place to Store Emails

We will want an email model which we can associate with a contact, so let’s create a model spec.

New file - spec/javascripts/unit/models/email_spec.js

1 module('Email model');

2

3 test('email address attribute', function () {

4 respondsTo('Email', 'address', 'string');

5 });

We’ll call the model email for simplicity, so we look for an email model, with an address attributeof type: ‘string’. We should have a failure similar to this:

Failure message

1 1) Email address model email address attribute (1, 0, 1)

2 Failure/Error: Died on test #1 at http://127.0.0.1:49285/assets/teaspoon\

3 -qunit.js?body=1:427

4 at http://127.0.0.1:49285/assets/models/email_spec.js?body=1:5: 'undefined' i\

5 s not an object (evaluating 'AddressBook.Email.metaForProperty')

Let’s now define our model.

Page 72: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 68

New file - /app/assets/javascripts/unit/models/email.js

1 AddressBook.Email = DS.Model.extend({

2 address: DS.attr('string'),

3 contact_id: DS.attr('number')

4 });

And with that, our model spec should be passing.

Testing Relationships

Now we will need to have a relationship between our contact and our email addresses. Each contactmay have zero or many email addresses, so let’s write a spec for that.

spec/javascripts/models/contacts_spec.js

1 //....

2 });

3 test('relationships', function () {

4 var emails = AddressBook.Contact.metaForProperty('emails');

5 ok(emails.isRelationship, 'Expecting isRelationship to be true, got false');

6 equal(emails.kind, 'hasMany', 'Expected a hasMany relationship got: ' + email\

7 s.kind);

8 });

With this test were are using some new concepts. metaForProperty('emails') looks for an emailsproperty on our model. We then test to see if .isRelationship is true. .kind returns a stringdescribing the relationship.

We will expect to see a failure similar to this:

Failure message

1 Failures:

2

3 1) Contacts Model relationships (1, 0, 1)

4 Failure/Error: Died on test #1 at http://127.0.0.1:50516/assets/teaspoon\

5 -qunit.js?body=1:427

6 at http://127.0.0.1:50516/assets/models/contacts_spec.js?body=1:15: Assertion\

7 Failed: metaForProperty() could not find a computed property with key 'emails'.

We can implement the relationship now by adding a new property to our Contact model.

Page 73: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 69

/app/assets/javascripts/models/contacts.js

1 last_name: DS.attr('string'),

2 emails: DS.hasMany('email')

And with that we should have a green spec for our relationship.

Rendering Relationships

We’ll now need a place to display our data so we need to update the show template.

/app/assets/javascripts/templates/contacts/show.hbs

1 </ul>

2 <ul>

3 {{#each emails}}

4 <li class="email_address">

5 {{address}}

6 </li>

7 {{/each}}

8 </ul>

Now in order to get our integration spec passing we will need some sample data to render. We willneed to update our fixtures in order to do this and in doing so we’ll discover a new gotcha.

spec/javascripts/spec_helper.js

1 var resetFixtures = function () {

2 AddressBook.Contact.FIXTURES = [

3 {

4 id: 1,

5 first_name: 'Dave',

6 last_name: 'Crack',

7 emails: [1,2]

8 },

9 //.....

10 ];

11 AddressBook.Email.FIXTURES = [

12 {

13 id: 1,

14 address: '[email protected]',

15 contact_id: 1

Page 74: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 70

16 },

17 {

18 id: 2,

19 address: '[email protected]',

20 contact_id: 1

21 }

22 ];

23 };

With this update to the fixtures we add an emails property to our first contact which specifies theid’s 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 Failure/Error: Error: Assertion Failed: You looked up the 'emails' relations\

2 hip on '<AddressBook.Contact:ember538:1>' but some of the associated records were\

3 not loaded. Either make sure they are all loaded together with the parent record\

4 , or specify that the relationship is async (`DS.hasMany({ async: true })`)

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 1) Email address integration Showing associated email addresses (1, 1, 2)

4 Failure/Error: Expected [email protected] got:

So we now have two elements on the page for email addresses, but we don’t have the correct text.The fix for this turns out to be kinda obscure. I’m not entirely sure that this approach would beconsidered best practice and it feels like a bit of a hack.

With that in mind, let’s remove the change to contacts.js

Page 75: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 71

/app/assets/javascripts/models/contacts.js

1 //...

2 emails: DS.hasMany('email')

We’ll next adjust the fixture reset function.

spec/javascripts/spec_helper.js

1 address: '[email protected]'

2 }

3 ];

4 AddressBook.Contact.reopen({

5 emails: DS.hasMany('email', {async: true})

6 });

7 //...

8 };

What we’re doing here is overwriting the hasMany property after the fixtures are createdwhich for some reason fixes the issue! I feel like this is the right place to make the hack asit will be easy to remove if this issue gets resolved in the future.

And with that, we should be green for everything again.

Time for a commit

Commit changes

1 git add .

2 git commit -m 'Add Email model and relationship'

Wiring Up the Relationships to Rails

So now we’re gonna need a place to store our emails in Rails, this will involve defining relationshipsand ways in which the should be serialised.

First we’ll add another testing gem. The shoulda matchers allow us to specify the existence ofrelationships on models, amongst other things.

Page 76: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 72

Gemfile

1 gem 'rspec-rails'

2 gem 'shoulda-matchers'

3 end

And install it.

Install gems

1 bundle install

An Email Model

We’ll need an email model, so let’s start with a failing spec.

New file - spec/models/email_spec.rb

1 require 'spec_helper'

2

3 describe Email do

4 let(:email) { FactoryGirl.build_stubbed(:email)}

5

6 subject { email }

7

8 it { should respond_to :address }

9 it { should belong_to :contact}

10

11 end

We should see an error similar to this

Failure message

1 address-book/spec/models/email_spec.rb:3:in `<top (required)>': uninitialized con\

2 stant Email (NameError)

Let’s fix that by creating our Email class.

Page 77: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 73

New file - /app/models/email.rb

1 class Email < ActiveRecord::Base

2 end

Good, now we should have a failure similar to this.

Failure message

1 Failures:

2

3 1) Email

4 Failure/Error: let(:email) { FactoryGirl.build_stubbed(:email)}

5 ActiveRecord::StatementInvalid:

6 Could not find table 'emails'

7 # ./spec/models/email_spec.rb:4:in `block (2 levels) in <top (required)>'

8 # ./spec/models/email_spec.rb:6:in `block (2 levels) in <top (required)>'

9 # ./spec/models/email_spec.rb:9:in `block (2 levels) in <top (required)>'

In order to get this passing we’ll need a table to store the emails, and we’ll create a migration forthat.

New file - /db/migrate/create_emails.rb

1 class CreateEmails < ActiveRecord::Migration

2 def change

3 create_table :emails do |t|

4 t.string :address

5 t.references :contact

6 end

7 end

8 end

If you’re fairly new to rails, the t.references may be new to you. This statement tells Rails thatthere is going to be a relationship with contacts and when the migration is run, it will create acolumn 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.

Page 78: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 74

Migrating

1 bundle exec rake db:migrate

2 bundle exec rake db:test:prepare

Our failure message should now be complaining about the missing factory.

Failure message

1 1) Email

2 Failure/Error: let(:email) { FactoryGirl.build_stubbed(:email)}

3 ArgumentError:

4 Factory not registered: email

Let’s create our factory.

New file - spec/factories/email.rb

1 FactoryGirl.define do

2 factory :email do

3 address "MyString"

4 end

5 end

You will need to execute the reload command in the guard session now to make the newfactory available.

Contact Relationship

Our failure should now tell us what we need to fix up next.

Page 79: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 75

Failure message

1 Failures:

2

3 1) Email should belong to contact

4 Failure/Error: it { should belong_to :contact}

5 Expected Email to have a belongs_to association called contact (no associa\

6 tion called contact)

7 # ./spec/models/email_spec.rb:9:in `block (2 levels) in <top (required)>'

Great, let’s get that fixed.

/app/models/email.rb

1 class Email < ActiveRecord::Base

2 belongs_to :contact

3 end

Email Relationship

We’ll need to specify the relationship in the opposite direction now, so we’ll add a new test to ourcontact spec.

spec/models/contact_spec.rb

1 it { should respond_to :last_name }

2 it { should have_many :emails }

Our failure message will now tell us what to do.

Failure message

1 Failures:

2

3 1) Contact should have many emails

4 Failure/Error: it { should have_many :emails }

5 Expected Contact to have a has_many association called emails (no associat\

6 ion called emails)

7 # ./spec/models/contact_spec.rb:10:in `block (2 levels) in <top (required)>'

Let’s get on an define the relationship

Page 80: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 76

/app/models/contact.rb

1 class Contact < ActiveRecord::Base

2 has_many :emails, dependent: :destroy

Awesome, we’re back to green.

Email Controller and Routes

Now we’re going to need a way to interact with our Email class so we will need a controller androute.

Let’s get started with a failing spec.

New file - spec/controllers/api/v1/emails_controller_spec.rb

1 require 'spec_helper'

2

3 describe Api::V1::EmailsController do

4 end

We should see a failure similar to this.

Failure message

1 address-book/spec/controllers/api/v1/emails_controller_spec.rb:3:in `<top (requir\

2 ed)>': uninitialized constant Api::V1::EmailsController (NameError)

And to fix it.

New file - app/controllers/api/v1/emails_controller.rb

1 class Api::V1::EmailsController < ApplicationController

2 end

Great, green again. Now to add a show test.

Page 81: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 77

spec/controllers/api/v1/emails_controller_spec.rb

1 describe Api::V1::EmailsController do

2 describe 'GET methods' do

3 it 'returns an email address' do

4 @email = FactoryGirl.create(:email)

5 get :show, id: @email.id

6 assigns(:email).class.should == Email

7 end

8 end

We’ll get a failure similar to this.

Failure message

1 No route matches {:id=>"1", :controller=>"api/v1/emails", :action=>"show"}

We can add the necessary route now.

/config/routes.rb

1 namespace :v1 do

2 resources :contacts

3 resources :emails

And now a new failure message

Failure message

1 Failures:

2

3 1) Api::V1::EmailsController GET methods returns an email address

4 Failure/Error: get :show, id: @email.id

5 AbstractController::ActionNotFound:

6 The action 'show' could not be found for Api::V1::EmailsController

7 # ./spec/controllers/api/v1/emails_controller_spec.rb:7:in `block (3 levels)\

8 in <top (required)>'

Let’s go ahead and fix that up.

Page 82: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 78

/app/controllers/api/v1/emails_controller.rb

1 class Api::V1::EmailsController < ApplicationController

2 before_action :set_email, only: [:show, :destroy]

3 def show

4 render json: @email

5 end

6

7 private

8

9 def set_email

10 @email = Email.find(params[:id])

11 end

12 end

We’ll also need a way to add an email, so let’s get that sorted

spec/controllers/api/v1/emails_controller_spec.rb

1 describe 'GET methods' do

2 #...

3 end

4 describe 'POST methods' do

5 it 'creates an email address' do

6 @email = FactoryGirl.attributes_for(:email)

7 expect{post :create, email: @email}.to change(Email, :count).by(1)

8 end

9 end

Failure message

1 Failures:

2

3 1) Api::V1::EmailsController POST methods creates an email address

4 Failure/Error: expect{post :create, email: @email}.to change(Email, :count).\

5 by(1)

6 AbstractController::ActionNotFound:

7 The action 'create' could not be found for Api::V1::EmailsController

OK, let’s fix that up.

Page 83: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 79

/app/controllers/api/v1/emails_controller.rb

1 # ...

2 def create

3 @email = Email.new(get_email_params)

4 if @email.save

5 render json: nil

6 else

7 render json @email.errors, status: :unprocessable_entity

8 end

9 end

10 private

11 #...

12 def get_email_params

13 params.require(:email).permit([:address, :contact_id])

14 end

And we’re back to green.

Next it would be good to be able to delete emails too.

spec/controllers/api/v1/emails_controller_spec.rb

1 describe 'DELETE methods' do

2 it 'deletes an email address' do

3 @email = FactoryGirl.create(:email)

4 expect{delete :destroy, id: @email.id}.to change(Email, :count).by(-1)

5 end

6 end

Failure message

1 1) Api::V1::EmailsController DELETE methods deletes an email address

2 Failure/Error: expect{delete :destroy, id: @email.id}.to change(Email, :coun\

3 t).by(-1)

4 AbstractController::ActionNotFound:

5 The action 'destroy' could not be found for Api::V1::EmailsController

OK, let’s fix that too.

Page 84: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 80

/app/controllers/api/v1/emails_controller.rb

1 def destroy

2 if @email.destroy

3 render json: nil

4 else

5 render json @email.errors, status: :unprocessable_entity

6 end

7 end

Serializing and Relationships

In order to pass our data to Ember we’ll need to update our serialisers and introduce the concept ofembedding relationships.

As our application grows we may want to embed many things, so we’ll make some adjustments thatwill reduce repetition in the future.

New file - /app/serializers/application_serializer.rb

1 class ApplicationSerializer < ActiveModel::Serializer

2 embed :ids, include: true

3 end

With this new class defined, we’ll get our other serializers to inherit from it. This tells Rails to usea particular convention for including associations. Essentially the object we’re looking for specifiedat the root level and it’s association id’s are specified as an array. Also at the root level we get theassociated records as an array of objects. Ember data does the work to reassemble the data on receipt.

Embedding Format

1 {

2 "contacts": {

3 "id": 1,

4 "first_name": "Dave",

5 "last_name": "Crack",

6 "emails": ["1", "2"]

7 },

8 "emails": [{

9 "id": "1",

10 "address": "[email protected]"

11 }, {

12 "id": "2",

Page 85: EmberJS Testing on Rails

Chapter 7 - Relationships in Ember 81

13 "address": "[email protected]"

14 }]

15 }

/app/serializers/contact_serializer.rb

1 class ContactSerializer < ApplicationSerializer

2 attributes :id, :first_name, :last_name

3 has_many :emails

4 end

New file - /app/serializers/email_serializer.rb

1 class EmailSerializer < ApplicationSerializer

2 attributes :id, :address, :contact_id

3 end

With that we’ve got everything we need to display our associated emails on the frontend.

Phew, time for a commit!

Commit

1 git add .

2 git commit -m 'Emails and serialization'

Up next, we’ll work on testing computed properties.

Page 86: EmberJS Testing on Rails

Chapter 8 - Unit Testing ComputedPropertiesSo in our contact index just having the contact’s first name isn’t really useful for navigation. LuckilyEmber gives us access to computed properties which can be kept in sync with the database andupdated whenever any of its base components change.

Let’s see how we can unit test that.

Computed properties are relatively straightforward as a concept, specify the computed propertyname and a function which returns the output you want and chain the properties that it depends onso that Ember can keep it up to date if any of those properties change.

In order to test them, we’ll need a record to work with and then test the output for our computedproperty.

I’m big on abstracting wherever possible, so let’s create a testing helper.

spec/javascripts/support/testing_helpers.js

1 //.....

2 var computedPropertyTest = function (model, record, computed_property, expected_o\

3 utput) {

4 var store = AddressBook.__container__.lookup('store:main');

5 Ember.run(function () {

6 var new_record = store.createRecord(model, record);

7 var computed = new_record.get(computed_property);

8 equal(computed, expected_output, 'Expected ' + expected_output +' got: ' \

9 + computed);

10 });

11 };

Below is an explanation of its usage

parameter expected input example

model The name of the model under test as astring

'full_name'

record A record to be created from an object {first_name: 'Dave', last_name:

'Crack'}computed_property The name of the computed property as

a string'full_name'

expected_output what we expect to see as a string 'Crack, Dave'

Page 87: EmberJS Testing on Rails

Chapter 8 - Unit Testing Computed Properties 83

And with that, we’re ready to write a failing spec.

spec/javascripts/unit/models/contacts_spec.js

1 // ....

2 });

3 test('full_name computed property', function () {

4 computedPropertyTest('contact', {first_name: 'Mabel', last_name: 'Smith'}, 'f\

5 ull_name', 'Smith, Mabel');

6 });

Which should tell us that result was undefined.

Failure message

1 Failures:

2

3 1) Contacts Model full_name computed property (1, 0, 1)

4 Failure/Error: Expected Smith, Mabel got: undefined

We can fix this up by adding our computed property.

/app/assets/javascripts/models/contacts.js

1 emails: DS.hasMany('email'),

2 full_name: function () {

3 return this.get('last_name') +', '+ this.get('first_name');

4 }.property()

Beautiful, we’ve got the spec passing. It would be great, however if the output got updated whenone of its dependencies changes.

Let’s test that.

Page 88: EmberJS Testing on Rails

Chapter 8 - Unit Testing Computed Properties 84

spec/javascripts/unit/models/contacts_spec.js

1 });

2 test('full name updates when properties change', function () {

3 var store = AddressBook.__container__.lookup('store:main');

4 Ember.run(function () {

5 var contact = store.createRecord('contact', {first_name: 'Buzz', last_nam\

6 e: 'Lightyear'});

7 var full_name = contact.get('full_name');

8 equal(full_name, 'Lightyear, Buzz', 'Expected "Lightyear, Buzz", got: ' +\

9 full_name);

10 contact.set('first_name', 'Slinky');

11 full_name = contact.get('full_name');

12 equal(full_name, 'Lightyear, Slinky', 'Expected "Lightyear, Slinky", got:\

13 '+ full_name);

14 contact.set('last_name', 'Dog');

15 full_name = contact.get('full_name');

16 equal(full_name, 'Dog, Slinky', 'Expected "Dog, Slinky", got: '+ full_nam\

17 e);

18 });

19 });

This should give us the following failure

Failure message

1 Failures:

2

3 1) Contacts Model full name updates when properties change (2, 1, 3)

4 Failure/Error: Expected "Lightyear, Slinky", got: Lightyear, Buzz

5

6 2) Contacts Model full name updates when properties change (2, 1, 3)

7 Failure/Error: Expected "Dog, Slinky", got: Lightyear, Buzz

This is pretty straightforward to resolve.

Page 89: EmberJS Testing on Rails

Chapter 8 - Unit Testing Computed Properties 85

/app/assets/javascripts/models/contacts.js

1 full_name: function () {

2 return this.get('last_name') +', '+ this.get('first_name');

3 }.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 test('Full name used for link on index page', function () {

2 visit('/contacts');

3 andThen(function () {

4 var first_contact = find('.contacts_list li:first a').text();

5 equal(first_contact, 'Crack, Dave', 'Expected "Crack, Dave", got: ' + fir\

6 st_contact);

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 {{#link-to 'contacts.show' this}}{{first_name}}{{/link-to}}

2 {{#link-to 'contacts.show' this}}{{full_name}}{{/link-to}}

And with that we’ve got another green spec!

Time to commit.

Commit changes

1 git add .

2 git commit -m 'Computed properties'

Page 90: EmberJS Testing on Rails

Chapter 9 - Creating and UpdatingEmails on the FrontendIn this chapter we’re going to take a dive into mocking our server responses. There’s going to be awhole stack of new things coming and a reasonable amount of refactoring.

It would be possible to do most of what we want to develop in our application without mockingserver responses, but this is a great opportunity to gain a deeper understanding of our API and howdata is passed back and forth in Ember.

Faking HTTP Requests

In order to provide a fake server, we’re going to use the work of Trek Glowacki ²¹.

There are a number of ways to do this, the simplest is to use wget, though you can visit the threerepos and download and copy and paste into new files.

All of the files will be created in /spec/javascripts/support

Fake server dependencies

1 cd /spec/javascripts/support

2 wget https://github.com/trek/FakeXMLHttpRequest/raw/master/fake_xml_http_request.\

3 js

4 wget https://raw.github.com/trek/fakehr/master/fakehr.js

5 wget https://raw.github.com/trek/ember-testing-httpRespond/master/httpRespond-1.1\

6 .js

With these files downloaded, let’s add them to the dependencies for teaspoon.

²¹The three repos we are using are https://github.com/trek/ember-testing-httpRespond, https://github.com/trek/fakehr, https://github.com/trek/FakeXMLHttpRequest

Page 91: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 87

spec/javascripts/spec_helper.js

1 //= require application.js.erb

2 //= require support/fake_xml_http_request

3 //= require support/httpRespond-1.1

4 //= require support/fakehr

5 //= require support/testing_helpers

6 //= require_self

By including these three files wemake a new construct available in our testing, .httpRespond("requesttype", "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.

Let’s see how this works by creating a new spec for frontend email testing.

New file - spec/javascripts/integration/frontend_email_spec.js

1 module('Frontend Email', {

2 setup: function () {

3 fakehr.start();

4 },

5 teardown: function () {

6 fakehr.stop();

7 }

8 });

This first step should look fairly familiar, with the addition of the fakehr start and stop calls. As thename 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 test("Add an email", function () {

4 var fakeContact = {contact: {id: 1, first_name: 'Dave', last_name: 'Crack', e\

5 mails: [1, 2]}, emails: [

6 {id: 1, address: '[email protected]'},

7 {id: 2, address: '[email protected]'}

8 ]};

9 visit('/contacts/1').httpRespond('get', '/api/v1/contacts/1', fakeContact);

10 });

Page 92: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 88

So there’s a lot going on here! First we create an object that will represent our contact using theformat that Rails will send it in:

• Top level - a contact with first name and last name strings, and an emails array which containstwo id’s

• Top level - an array containing email objects

From there we visit the correct route /contacts/1 and add the new httpRespond() function. We tellit to expect a get request to /api/v1/contacts/1 and to return the fakeContact. Now we haven’tactually issued any expectations at this point, but let’s run the test anyway.

Failure message

1 Failures:

2

3 1) Frontend Email Add an email (1, 0, 1)

4 Failure/Error: Error: No request intercepted for GET /api/v1/contacts/1. Int\

5 ercepted requests were:

When we use the httpRespond() function is sets an expectation that it will be called. Now we knowthat when we visit /contacts/1‘ Ember should be requesting data from the server, so what’s gonewrong? Well the problem is that we earlier setup a FixtureAdapter which means that no serverrequests are happening!

In order to resolve this we’re going to ensure we have functions to turn on the RESTAdapter or theFixtureAdapter as necessary. We’ll 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 var resetFixtures = function () {

4 AddressBook.Contact.FIXTURES = [

5 { id: 1, first_name: 'Dave', last_name: 'Crack', emails: [1,2] },

6 { id: 2, first_name: 'Dustin', last_name: 'Hoffman' }

7 ];

8 AddressBook.Email.FIXTURES = [

9 { id: 1, address: '[email protected]', contact_id: 1 },

10 { id: 2, address: '[email protected]', contact_id: 1 }

11 ];

12 AddressBook.Contact.reopen({

13 emails: DS.hasMany('email', {async: true})

14 });

15 };

Page 93: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 89

We will now add this to our testing_helpers

spec/javascripts/support/testing_helpers.js

1 //..

2 var resetFixtures = function () {

3 AddressBook.ApplicationAdapter = DS.FixtureAdapter;

4 AddressBook.Contact.FIXTURES = [

5 { id: 1, first_name: 'Dave', last_name: 'Crack', emails: [1, 2] },

6 { id: 2, first_name: 'Dustin', last_name: 'Hoffman' }

7 ];

8 AddressBook.Email.FIXTURES = [

9 { id: 1, address: '[email protected]', contact_id: 1 },

10 { id: 2, address: '[email protected]', contact_id: 1 }

11 ];

12 AddressBook.Contact.reopen({

13 emails: DS.hasMany('email', {async: true})

14 });

15 };

Next we’ll ensure that we turn have the fixture adapter turned back on after our frontend_email_-spec.

spec/javascripts/integration/frontend_email_spec.js

1 teardown: function () {

2 fakehr.stop();

3 resetFixtures();

Reimplementing RESTAdapter in Tests

Now let’s create a function to turn our RESTApdapter back on.

Page 94: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 90

spec/javascripts/support/testing_helpers.js

1 //..

2 var turnOnRESTAdapter = function () {

3 AddressBook.ApplicationAdapter = DS.RESTAdapter;

4

5 AddressBook.Store = DS.Store.extend({

6 adapter: '-active-model'

7 });

8

9 DS.RESTAdapter.reopen(

10 {namespace: "api/v1"}

11 );

12 };

What we are doing here is specifying that the ApplicationAdapter uses the DS.RESTAdapter, tellingthe Store to use the active-model structure for serialising the data and telling the RESTAdapter toprepend 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 setup: function () {

2 turnOnRESTAdapter();

3 //..

Great, now we should see QUnit complaining about no assertions being specified.

Failure message

1 Failures:

2

3 1) Frontend Email Add an email (1, 0, 1)

4 Failure/Error: Expected at least one assertion, but none were run - call exp\

5 ect(0) to accept zero assertions.

Now I’ve specified in our mock JSON data that we should have an email ‘[email protected]’ whichis different to our Fixture data, let’s test for that to make sure the data is getting rendered.

Page 95: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 91

spec/javascripts/integration/frontend_email_spec.js

1 visit('/contacts/1').httpRespond('get', '/api/v1/contacts/1', fakeContact);

2 andThen(function () {

3 var email = /[email protected]/.test($('li').text());

4 ok(email, 'Expected to find [email protected]');

5 )};

So here we introduce a new way of checking that text can be found in the page by using aRegularExpression test. This takes in a jQuery object on which we call text() and returns true if thetext is found. The same effect can be achieved with the find helper, but it’s great to have choices!

With that, our tests should be green.

Now let’s start adding the behaviour we want to add a new email.

We’ve seen earlier that we can use a boolean to set a state and use this in the view to determinewhat is rendered, so let’s create the behaviour we want attached to a button which will then renderan input field where the user can enter a new email address.

We’ll start with the button.

spec/javascripts/integration/frontend_email_spec.js

1 ok(email, 'Expected to find [email protected]');

2 click('#create_email');

3 var input = find('#new_email').length;

4 ok(input, 'Expected to find new email input');

We are asking for or spec to look for a element with an id of ‘create_email’, click the button andthen 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 1) Frontend Email Add an email (1, 1, 2)

4 Failure/Error: Error: Element #create_email not found.

In our template we can now add the button in an if/else block.

Page 96: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 92

/app/assets/javascripts/templates/contacts/show.hbs

1 {{#if addingEmail}}

2 {{else}}

3 <button id="create_email" {{action 'createEmail'}} >Add new email</button>

4 {{/if}}

5 <ul>

6 {{#each emails}}

So here we are stating that we should check truthiness of addingEmail and if it’s false, display abutton. We assign the action createEmail to the button.

Our tests should now be guiding us where to go.

Failure message

1 Failures:

2

3 1) Frontend Email Add an email (1, 1, 2)

4 Failure/Error: Error: Nothing handled the action 'createEmail'. If you did h\

5 andle the action, this error can be caused by returning true from an action handl\

6 er in a controller, causing the action to bubble.

Now the correct place to handle this behaviour would be in a ContactShowController, so let’s getthat created.

New file - /app/assets/javascripts/controllers/contacts_show_controller.js

1 AddressBook.ContactsShowController = Ember.ObjectController.extend({

2 actions: {

3 createEmail: function () {

4 this.toggleProperty('addingEmail');

5 }

6 }

7 });

We use the Ember.ObjectController.extend constructor for this controller because we are dealingwith a single item, rather than a collection.

Our tests should now be telling us that the input field cannot be found.

Page 97: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 93

Failure message

1 1) Frontend Email Add an email (1, 1, 2)

2 Failure/Error: Expected to find new email input

Now we can add the input field.

/app/assets/javascripts/templates/contacts/show.hbs

1 {{#if addingEmail}}

2 <label for="new_email">Email address</label>

3 {{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 didn’t need to specify a new_email object on the controller or to set a booleanor 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 ok(input, 'Expected to find new email input');

2 fillIn('#new_email', '[email protected]');

3 keyEvent('#new_email', 'keyup', 13).httpRespond('post', '/api/v1/emails', {}, 200\

4 );

We’ve used the fillIn behaviour before so that’s straightforward.

The keyEvent helper is a new one, which allows us to simulate a key event. The function is calledlike this - keyEvent(selector, type, keyCode). Let’s explain that a bit further.

Parameter Expected input

selector A string to find the element on which to simulate a key eventtype A string describing the javascript key event (such as ‘keyup’, ‘keydown’, ‘keypress’)keyCode A number which represents the key pressed²², such as 13 which represents the enter key.

We now know that we are expecting the enter button to be pressed to save the email. This seemslike 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

Page 98: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 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 returnwith 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 1) Frontend Email Add an email (1, 2, 3)

4 Failure/Error: Error: No request intercepted for POST /api/v1/emails. Interc\

5 epted requests were: GET /api/v1/contacts/1

There’s 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 {{#if addingEmail}}

2 <label for="new_email">Email address</label>

3 {{input type='text' value=controller.new_email id='new_email' insert-newl\

4 ine='saveNewEmail'}}

Here we update the input to bind insert-newline to a saveNewEmail action.

This won’t get our test passing, we’ll still need to add the behaviour to our controller.

Saving Associations

This bit is fairly involved, so I’ll explain it in more detail afterwards.

/app/assets/javascripts/controllers/contacts_show_controller.js

1 },

2 saveNewEmail: function () {

3 //1

4 var self, emailStore, newEmail, contactID, record;

5 //2

6 self = this;

7 //3

8 emailStore = this.store;

9 //4

10 newEmail = this.get('new_email');

Page 99: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 95

11 //5

12 contactID = this.get('id');

13 //6

14 record = emailStore.createRecord('email', {address: newEmail, contact\

15 _id: contactID});

16 //7

17 var onFulfillment = function (data) {

18 self.toggleProperty('addingEmail');

19 self.set('new_email', '');

20 };

21 //8

22 record.save().then(onFulfillment);

23 }

24 }

25 });

1. We declare a bunch of variable we are going to use2. self is to store a reference to this which we’ll use later when our promise is fulfilled3. emailStore gets a reference to our data store4. newEmail gets the value of new_email5. contactID gets the ID of our contact (we need this to associate the email with the correct

contact)6. We create a new email record passing in an object with the data we want to store7. We create a function that will be called if the record saves successfully. It sets addingEmail to

false and new_email to an empty string.8. We call save() on our record and specify that onFulfillment should be called if the save

succeeds in .then()

Phew! That was a lot of work. The reason that things are a bit more involved here is because we’renot actually working directly with the emails, but rather accessing them through their parent object.Because of this we need to be much more explicit than when we were creating a new contact earlier.

Now our tests are green again. We should check that our data is now displayed on the screen.

Page 100: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 96

spec/javascripts/integration/frontend_email_spec.js

1 keyEvent('#new_email', 'keyup', 13).httpRespond('post', '/api/v1/emails',\

2 {email: {id: 3, contact_id: 1, address: '[email protected]'}}, 200);

3 andThen(function () {

4 ok(/[email protected]/.test($('.email_address').text()), 'Expecte\

5 d to find [email protected]');

6 });

Which should fail with the following

Failure message

1 Failures:

2

3 1) Frontend Email Add an email (1, 2, 3)

4 Failure/Error: Expected to find [email protected]

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 self.set('new_email', '');

2 var localEmails = self.get('model.emails');

3 localEmails.pushObject(data);

Our tests should once again be green.

Dealing With Failed Saves

The next obvious thing to deal with would be the scenario where the request to save the item fails.We’ll move our fakeContact variable outside the test so it’s available to all tests and write the specfor failure.

Page 101: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 97

spec/javascripts/integration/frontend_email_spec.js

1 var fakeContact = {contact: {id: 1, first_name: 'Dave', last_name: 'Crack', email\

2 s: [1, 2]}, emails: [

3 {id: 1, address: '[email protected]'},

4 {id: 2, address: '[email protected]'}

5 ]};

6

7 test("Add an email", function () {

8 var fakeContact = {contact: {id: 1, first_name: 'Dave', last_name: 'Crack', e\

9 mails: [1, 2]}, emails: [

10 {id: 1, address: '[email protected]'},

11 {id: 2, address: '[email protected]'}

12 ]};

13 visit('/contacts/1').httpRespond('get', '/api/v1/contacts/1', fakeContact);

14 //....

15 });

16 test("Tell user when failed to save email", function () {

17 visit('/contacts/1').httpRespond('get', '/api/v1/contacts/1', fakeContact);

18 andThen(function () {

19 click('#create_email');

20 fillIn('#new_email', '[email protected]');

21 keyEvent('#new_email', 'keyup', 13).httpRespond('post', '/api/v1/emails',\

22 {}, 400);

23 andThen(function () {

24 ok(/Failed to save/.test($('.error').text()), 'Expected to find "Fail\

25 ed to save"');

26 });

27 });

28 });

Here we specify that when we hit the enter button we expect that a post request is made and thatit will return a black response with a 400 failure code. This should trigger the display of a messagetelling the user that the save failed.

To implement this we’ll need a place in our template to render the message.

Page 102: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 98

/app/assets/javascripts/templates/contacts/show.hbs

1 {{#if failedToSave}}

2 <div>

3 <p class="error">{{failedToSaveMessage}}</p>

4 </div>

5 {{/if}}

6 {{#if addingEmail}}

We can now update the controller to handle the error.

/app/assets/javascripts/controllers/contacts_show_controller.js

1 localEmails.pushObject(data);

2 };

3 var onRejection = function () {

4 self.toggleProperty('failedToSave');

5 self.set('failedToSaveMessage','Failed to save email, please try later');

6 };

7 record.save().then(onFulfillment, onRejection);

With this we creating a failedToSave boolean and setting it to true and creating a failedToSaveMessagestring. Green specs again, but what happens if the user then proceeds to try and save again and thistime it succeeds?

Let’s update our spec to allow for a success following failure.

spec/javascripts/integration/frontend_email_spec.js

1 test("Tell user when failed to save email", function () {

2 visit('/contacts/1').httpRespond('get', '/api/v1/contacts/1', fakeContact);

3 var textToFind = /Failed to save/;

4 andThen(function () {

5 click('#create_email');

6 fillIn('#new_email', '[email protected]');

7 keyEvent('#new_email', 'keyup', 13).httpRespond('post', '/api/v1/emails',\

8 {}, 400);

9 andThen(function () {

10 ok(textToFind.test($('.error').text()), 'Expected to find "Failed to \

11 save"');

12 keyEvent('#new_email', 'keyup', 13).httpRespond('post', '/api/v1/emai\

13 ls', {email: {id: 3, contact_id: 1, address: '[email protected]'}}, 200);

14 andThen(function () {

Page 103: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 99

15 ok(!textToFind.test($('.error').text()), 'Did not expect to find \

16 "Failed to save"');

17 });

18 });

19 });

20 });

Now that we’re testing the text twice we pull it out into a variable, then we add another keyEventwith a successful response and assert that we should not see the error message any more.

/app/assets/javascripts/controllers/contacts_show_controller.js

1 var resetErrorMessages = function () {

2 if (self.get('failedToSave')) {

3 self.toggleProperty('failedToSave');

4 self.set('failedToSaveMessage', '');

5 }

6 }

7 var onFulfillment = function (data) {

8 //...

9 localEmails.pushObject(data);

10 resetErrorMessages();

11 };

And with that we’ve got a useful message for our users if the save fails and way of dealing withsuccess after failure.

Now would be a good time to commit our changes before we move on to connecting this up to thebackend.

Commit changes

1 git add .

2 git commit -m 'Frontend email specs and implementation'

Testing JSON Responses in Rails

Now the observant ones among you would have noticed that in the specs we’ve just worked throughwe mocked a response that returned the newly created email, but when we implemented it earlierin the Rails controller, we were returning a blank, but successful, response. We’ll create a spec forthat now.

Page 104: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 100

spec/controllers/api/v1/emails_controller_spec.rb

1 describe 'POST methods' do

2 it 'creates an email address' do

3 //..

4 end

5 it 'returns the email as JSON' do

6 email = FactoryGirl.attributes_for(:email)

7 post :create, email: email

8 body = JSON.parse(response.body)['email']

9 %w(address contact_id id).each do |attribute|

10 expect(body).to have_key(attribute)

11 end

12 end

Here we are using a tiny bit of meta-programming to make the code a little bit shorter. This alsomeans we can easily update the test in the future if we want to make any changes.

Failure message

1 Failures:

2

3 1) Api::V1::EmailsController POST methods returns the email as JSON

4 Failure/Error: body = JSON.parse(response.body)['email']

5 JSON::ParserError:

6 757: unexpected token at 'null'

7 # ./spec/controllers/api/v1/emails_controller_spec.rb:21:in `block (3 levels\

8 ) in <top (required)>'

This is because we are not actually returning any json currently, only a success code. Let’s get thatsorted, and a I apologise for how much work this is going to take….

/app/controllers/api/v1/emails_controller.rb

1 def create

2 @email = Email.new(get_email_params)

3 if @email.save

4 render json: @email

We’ll commit our changes now.

Page 105: EmberJS Testing on Rails

Chapter 9 - Creating and Updating Emails on the Frontend 101

Commit changes

1 git add .

2 git commit -m 'Creating emails specs and implementation'

In the next chapter we’ll have a look at updating our contacts and their email addresses.