Presented at Confoo (Montreal, Cananda)
Let's spend some time seeing how easy it can be to set up Mocha and Chai, a testing framework for JavaScript/CoffeeScript, in your application. We'll learn how to test that our jQuery or Backbone code is doing what it supposed to. It's really not as hard as you think it might be.
12. app/models/todo.rb
class Todo < ActiveRecord::Base
validates :body, presence: true
attr_accessible :body, :completed
end
Monday, February 25, 13
13. spec/models/todo_spec.rb
require 'spec_helper'
describe Todo do
it "requires a body" do
todo = Todo.new
todo.should_not be_valid
todo.errors[:body].should include("can't be blank")
todo.body = "Do something"
todo.should be_valid
end
end
Monday, February 25, 13
14. app/controllers/todos_controller.rb
class TodosController < ApplicationController
respond_to :html, :json
def index
respond_to do |format|
format.html {}
format.json do
@todos = Todo.order("created_at asc")
respond_with @todos
end
end
end
def show
@todo = Todo.find(params[:id])
respond_with @todo
end
def create
@todo = Todo.create(params[:todo])
respond_with @todo
end
def update
@todo = Todo.find(params[:id])
@todo.update_attributes(params[:todo])
respond_with @todo
end
def destroy
@todo = Todo.find(params[:id])
@todo.destroy
respond_with @todo
end
end
Monday, February 25, 13
15. spec/controllers/todos_controller_spec.rb
require 'spec_helper'
it "responds with errors" do
expect {
describe TodosController do
post :create, todo: {}, format: 'json'
let(:todo) { Factory(:todo) }
response.should_not be_successful
json = decode_json(response.body)
describe 'index' do
json.errors.should have(1).error
json.errors.body.should include("can't be blank")
context "HTML" do
}.to_not change(Todo, :count)
end
it "renders the HTML page" do
get :index
end
response.should render_template(:index)
end
assigns(:todos).should be_nil
end
describe 'update' do
end
context "JSON" do
context "JSON" do
it "updates a todo" do
put :update, id: todo.id, todo: {body: "do something else"}, format: 'json'
it "returns JSON for the todos" do
get :index, format: "json"
response.should be_successful
todo.reload
response.should_not render_template(:index)
todo.body.should eql "do something else"
assigns(:todos).should_not be_nil
end
end
it "responds with errors" do
end
put :update, id: todo.id, todo: {body: ""}, format: 'json'
end
response.should_not be_successful
json = decode_json(response.body)
describe 'show' do
json.errors.should have(1).error
json.errors.body.should include("can't be blank")
context "JSON" do
end
it "returns the todo" do
end
get :show, id: todo.id, format: 'json'
end
response.should be_successful
response.body.should eql todo.to_json
describe 'destroy' do
end
context "JSON" do
end
it "destroys the todo" do
end
todo.should_not be_nil
expect {
describe 'create' do
delete :destroy, id: todo.id, format: 'JSON'
}.to change(Todo, :count).by(-1)
context "JSON" do
end
it "creates a new todo" do
end
expect {
post :create, todo: {body: "do something"}, format: 'json'
end
response.should be_successful
end
}.to change(Todo, :count).by(1)
end
Monday, February 25, 13
16. app/views/todos/index.html.erb
<form class='form-horizontal' id='todo_form'></form>
<ul id='todos' class="unstyled"></ul>
<script>
$(function() {
new OMG.Views.TodosApp();
})
</script>
Monday, February 25, 13
30. ASSERTIONS/MATCHERS
• to (should) • .ok • .instanceof(constructor)
• be • .true • .property(name, [value])
• been • .false • .ownProperty(name)
• is • .null • .length(value)
• that • .undefined • .match(regexp)
• and • .exist • .string(string)
• have • .empty • .keys(key1, [key2], [...])
• with • .equal (.eql) • .throw(constructor)
• .deep • .above(value) • .respondTo(method)
• .a(type) • .below(value) • .satisfy(method)
• .include(value) • .within(start, finish) • .closeTo(expected, delta)
Monday, February 25, 13
31. MOCHA/CHAI WITH RAILS
• gem 'konacha'
• gem 'poltergiest' (brew install phantomjs)
Monday, February 25, 13
32. config/initializers/konacha.rb
if defined?(Konacha)
require 'capybara/poltergeist'
Konacha.configure do |config|
config.spec_dir = "spec/javascripts"
config.driver = :poltergeist
end
end
Monday, February 25, 13
37. spec/javascripts/spec_helper.coffee
# Require the appropriate asset-pipeline files:
#= require application
# Any other testing specific code here...
# Custom matchers, etc....
# Needed for stubbing out "window" properties
# like the confirm dialog
Konacha.mochaOptions.ignoreLeaks = true
beforeEach ->
@page = $("#konacha")
Monday, February 25, 13
38. app/assets/javascript/greeter.js.coffee
class @Greeter
constructor: (@name) ->
unless @name?
throw new Error("You need a name!")
greet: ->
"Hi #{@name}"
Monday, February 25, 13
39. spec/javascripts/greeter_spec.coffee
#= require spec_helper
describe "Greeter", ->
describe "initialize", ->
it "raises an error if no name", ->
expect(-> new Greeter()).to.throw("You need a name!")
describe "greet", ->
it "greets someone", ->
greeter = new Greeter("Mark")
greeter.greet().should.eql("Hi Mark")
Monday, February 25, 13
45. spec/javascripts/spec_helper.coffee
# Require the appropriate asset-pipeline files:
#= require application
#= require_tree ./support
# Any other testing specific code here...
# Custom matchers, etc....
# Needed for stubbing out "window" properties
# like the confirm dialog
Konacha.mochaOptions.ignoreLeaks = true
beforeEach ->
@page = $("#konacha")
Monday, February 25, 13
46. app/assets/javascripts/views/todo_view.js.coffee
class OMG.Views.TodoView extends OMG.Views.BaseView
tagName: 'li'
template: JST['todos/_todo']
events:
'change [name=completed]': 'completedChecked'
'click .delete': 'deleteClicked'
initialize: ->
@model.on "change", @render
@render()
render: =>
$(@el).html(@template(todo: @model))
if @model.get("completed") is true
@$(".todo-body").addClass("completed")
@$("[name=completed]").attr("checked", true)
return @
completedChecked: (e) =>
@model.save(completed: $(e.target).attr("checked")?)
deleteClicked: (e) =>
e?.preventDefault()
if confirm("Are you sure?")
@model.destroy()
$(@el).remove()
Monday, February 25, 13
47. spec/javascripts/views/todos/todo_view_spec.coffee
#= require spec_helper
describe "OMG.Views.TodoView", ->
beforeEach ->
@collection = new OMG.Collections.Todos()
@model = new OMG.Models.Todo(id: 1, body: "Do something!", completed: false)
@view = new OMG.Views.TodoView(model: @model, collection: @collection)
@page.html(@view.el)
Monday, February 25, 13
48. spec/javascripts/views/todos/todo_view_spec.coffee
describe "model bindings", ->
it "re-renders on change", ->
$('.todo-body').should.have.text("Do something!")
@model.set(body: "Do something else!")
$('.todo-body').should.have.text("Do something else!")
Monday, February 25, 13
49. spec/javascripts/views/todos/todo_view_spec.coffee
describe "displaying of todos", ->
it "contains the body of the todo", ->
$('.todo-body').should.have.text("Do something!")
it "is not marked as completed", ->
$('[name=completed]').should.not.be.checked
$('.todo-body').should.not.have.class("completed")
describe "completed todos", ->
beforeEach ->
@model.set(completed: true)
it "is marked as completed", ->
$('[name=completed]').should.be.checked
$('.todo-body').should.have.class("completed")
Monday, February 25, 13
50. spec/javascripts/views/todos/todo_view_spec.coffee
describe "checking the completed checkbox", ->
beforeEach ->
$('[name=completed]').should.not.be.checked
$('[name=completed]').click()
it "marks it as completed", ->
$('[name=completed]').should.be.checked
$('.todo-body').should.have.class("completed")
describe "unchecking the completed checkbox", ->
beforeEach ->
@model.set(completed: true)
$('[name=completed]').should.be.checked
$('[name=completed]').click()
it "marks it as not completed", ->
$('[name=completed]').should.not.be.checked
$('.todo-body').should.not.have.class("completed")
Monday, February 25, 13
51. app/assets/javascripts/todos/todo_view.coffee
class OMG.Views.TodoView extends OMG.Views.BaseView
# ...
deleteClicked: (e) =>
e?.preventDefault()
if confirm("Are you sure?")
@model.destroy()
$(@el).remove()
Monday, February 25, 13
52. spec/javascripts/views/todos/todo_view_spec.coffee
describe "clicking the delete button", ->
describe "if confirmed", ->
it "will remove the todo from the @page", ->
@page.html().should.contain($(@view.el).html())
$(".delete").click()
@page.html().should.not.contain($(@view.el).html())
describe "if not confirmed", ->
it "will not remove the todo from the @page", ->
@page.html().should.contain($(@view.el).html())
$(".delete").click()
@page.html().should.contain($(@view.el).html())
Monday, February 25, 13
57. spec/javascripts/spec_helper.coffee
# Require the appropriate asset-pipeline files:
#= require application
#= require support/sinon
#= require_tree ./support
# Any other testing specific code here...
# Custom matchers, etc....
# Needed for stubbing out "window" properties
# like the confirm dialog
Konacha.mochaOptions.ignoreLeaks = true
beforeEach ->
@page = $("#konacha")
@sandbox = sinon.sandbox.create()
afterEach ->
@sandbox.restore()
Monday, February 25, 13
58. spec/javascripts/views/todos/todo_view_spec.coffee
describe "clicking the delete button", ->
describe "if confirmed", ->
beforeEach ->
@sandbox.stub(window, "confirm").returns(true)
it "will remove the todo from the @page", ->
@page.html().should.contain($(@view.el).html())
$(".delete").click()
@page.html().should.not.contain($(@view.el).html())
describe "if not confirmed", ->
beforeEach ->
@sandbox.stub(window, "confirm").returns(false)
it "will not remove the todo from the @page", ->
@page.html().should.contain($(@view.el).html())
$(".delete").click()
@page.html().should.contain($(@view.el).html())
Monday, February 25, 13
60. app/assets/javascripts/views/todos/todo_list_view.js.coffee
class OMG.Views.TodosListView extends OMG.Views.BaseView
el: "#todos"
initialize: ->
@collection.on "reset", @render
@collection.on "add", @renderTodo
@collection.fetch()
render: =>
$(@el).html("")
@collection.forEach (todo) =>
@renderTodo(todo)
renderTodo: (todo) =>
view = new OMG.Views.TodoView(model: todo, collection: @collection)
$(@el).prepend(view.el)
Monday, February 25, 13
61. spec/javascripts/views/todos/todo_list_view_spec.coffee
#= require spec_helper
describe "OMG.Views.TodosListView", ->
beforeEach ->
@page.html("<ul id='todos'></ul>")
@collection = new OMG.Collections.Todos()
@view = new OMG.Views.TodosListView(collection: @collection)
it "fetches the collection", ->
@collection.should.have.length(2)
it "renders the todos from the collection", ->
el = $(@view.el).html()
el.should.match(/Do something!/)
el.should.match(/Do something else!/)
it "renders new todos added to the collection", ->
@collection.add(new OMG.Models.Todo(body: "Do another thing!"))
el = $(@view.el).html()
el.should.match(/Do another thing!/)
Monday, February 25, 13
67. spec/javascripts/views/todos/todo_list_view_spec.coffee
#= require spec_helper
describe "OMG.Views.TodosListView", ->
beforeEach ->
@page.html("<ul id='todos'></ul>")
@collection = new OMG.Collections.Todos()
@view = new OMG.Views.TodosListView(collection: @collection)
it "fetches the collection", ->
@collection.should.have.length(2)
it "renders the todos from the collection", ->
el = $(@view.el).html()
el.should.match(/Do something!/)
el.should.match(/Do something else!/)
it "renders new todos added to the collection", ->
@collection.add(new OMG.Models.Todo(body: "Do another thing!"))
el = $(@view.el).html()
el.should.match(/Do another thing!/)
Monday, February 25, 13
68. spec/javascripts/views/todos/todo_list_view_spec.coffee
#= require spec_helper
describe "OMG.Views.TodosListView", ->
beforeEach ->
@page.html("<ul id='todos'></ul>")
@collection = new OMG.Collections.Todos()
@view = new OMG.Views.TodosListView(collection: @collection)
MockServer.respond()
it "fetches the collection", ->
@collection.should.have.length(2)
it "renders the todos from the collection", ->
el = $(@view.el).html()
el.should.match(/Do something!/)
el.should.match(/Do something else!/)
it "renders new todos added to the collection", ->
@collection.add(new OMG.Models.Todo(body: "Do another thing!"))
el = $(@view.el).html()
el.should.match(/Do another thing!/)
Monday, February 25, 13