testing your javascript & coffeescript
DESCRIPTION
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.TRANSCRIPT
TESTING RICH *SCRIPT APPLICATIONS WITH RAILS
@markbates
Monday, February 25, 13
Monday, February 25, 13
http://www.metacasts.tvCONFOO2013
Monday, February 25, 13
Monday, February 25, 13
Monday, February 25, 13
Monday, February 25, 13
Finished in 4.41041 seconds108 examples, 0 failures
Monday, February 25, 13
Monday, February 25, 13
Monday, February 25, 13
A QUICK POLL
Monday, February 25, 13
Monday, February 25, 13
app/models/todo.rbclass Todo < ActiveRecord::Base
validates :body, presence: true
attr_accessible :body, :completed
end
Monday, February 25, 13
spec/models/todo_spec.rbrequire '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
app/controllers/todos_controller.rbclass 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
spec/controllers/todos_controller_spec.rbrequire 'spec_helper'
describe TodosController do
let(:todo) { Factory(:todo) }
describe 'index' do context "HTML" do it "renders the HTML page" do get :index
response.should render_template(:index) assigns(:todos).should be_nil end
end
context "JSON" do it "returns JSON for the todos" do get :index, format: "json"
response.should_not render_template(:index) assigns(:todos).should_not be_nil end
end
end
describe 'show' do context "JSON" do it "returns the todo" do get :show, id: todo.id, format: 'json'
response.should be_successful response.body.should eql todo.to_json end
end
end
describe 'create' do context "JSON" do it "creates a new todo" do expect { post :create, todo: {body: "do something"}, format: 'json'
response.should be_successful }.to change(Todo, :count).by(1) end
it "responds with errors" do expect { post :create, todo: {}, format: 'json'
response.should_not be_successful json = decode_json(response.body) json.errors.should have(1).error json.errors.body.should include("can't be blank") }.to_not change(Todo, :count) end
end
end
describe 'update' do context "JSON" do it "updates a todo" do put :update, id: todo.id, todo: {body: "do something else"}, format: 'json'
response.should be_successful todo.reload todo.body.should eql "do something else" end
it "responds with errors" do put :update, id: todo.id, todo: {body: ""}, format: 'json'
response.should_not be_successful json = decode_json(response.body) json.errors.should have(1).error json.errors.body.should include("can't be blank") end
end
end
describe 'destroy' do context "JSON" do it "destroys the todo" do todo.should_not be_nil expect { delete :destroy, id: todo.id, format: 'JSON' }.to change(Todo, :count).by(-1) end
end
end
end
Monday, February 25, 13
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
SO WHERE’S THE CODE?
Monday, February 25, 13
app/assets/javascripts/views/todo_view.js.coffeeclass 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
HOW DO WE TEST THIS?
Monday, February 25, 13
CAPYBARA?
Monday, February 25, 13
CAPYBARA?XMonday, February 25, 13
Mocha Chai+ =
Monday, February 25, 13
Mocha Chai+ =
Monday, February 25, 13
Monday, February 25, 13
Monday, February 25, 13
JavaScript example:
CoffeeScript example:
describe('panda', function(){ it('is happy', function(){ panda.should.be("happy") });});
describe 'panda', -> it 'is happy', -> panda.should.be("happy")
Monday, February 25, 13
Monday, February 25, 13
EXPECT/SHOULD/ASSERTexpect(panda).to.be('happy')panda.should.be("happy")assert.equal(panda, 'happy')
expect(foo).to.be.truefoo.should.be.trueassert.isTrue(foo)
expect(foo).to.be.nullfoo.should.be.nullassert.isNull(foo)
expect([]).to.be.empty[].should.be.emptyassert.isEmpty([])
Monday, February 25, 13
Monday, February 25, 13
ASSERTIONS/MATCHERS• to (should)
• be
• been
• is
• that
• and
• have
• with
• .deep
• .a(type)
• .include(value)
• .ok
• .true
• .false
• .null
• .undefined
• .exist
• .empty
• .equal (.eql)
• .above(value)
• .below(value)
• .within(start, finish)
• .instanceof(constructor)
• .property(name, [value])
• .ownProperty(name)
• .length(value)
• .match(regexp)
• .string(string)
• .keys(key1, [key2], [...])
• .throw(constructor)
• .respondTo(method)
• .satisfy(method)
• .closeTo(expected, delta)
Monday, February 25, 13
MOCHA/CHAI WITH RAILS
• gem 'konacha'
• gem 'poltergiest' (brew install phantomjs)
Monday, February 25, 13
config/initializers/konacha.rbif defined?(Konacha) require 'capybara/poltergeist' Konacha.configure do |config| config.spec_dir = "spec/javascripts" config.driver = :poltergeist endend
Monday, February 25, 13
rake konacha:serve
Monday, February 25, 13
Monday, February 25, 13
Monday, February 25, 13
LET’S WRITE A TEST!
Monday, February 25, 13
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 dialogKonacha.mochaOptions.ignoreLeaks = true
beforeEach -> @page = $("#konacha")
Monday, February 25, 13
app/assets/javascript/greeter.js.coffeeclass @Greeter
constructor: (@name) -> unless @name? throw new Error("You need a name!")
greet: -> "Hi #{@name}"
Monday, February 25, 13
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
Monday, February 25, 13
NOW THE HARD STUFF
Monday, February 25, 13
Monday, February 25, 13
chai-jqueryhttps://github.com/chaijs/chai-jquery
Monday, February 25, 13
MATCHERS• .attr(name[, value])
• .data(name[, value])
• .class(className)
• .id(id)
• .html(html)
• .text(text)
• .value(value)
• .visible
• .hidden
• .selected
• .checked
• .disabled
• .exist
• .match(selector) / .be(selector)
• .contain(selector)
• .have(selector)
Monday, February 25, 13
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 dialogKonacha.mochaOptions.ignoreLeaks = true
beforeEach -> @page = $("#konacha")
Monday, February 25, 13
app/assets/javascripts/views/todo_view.js.coffeeclass 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
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
spec/javascripts/views/todos/todo_view_spec.coffeedescribe "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
spec/javascripts/views/todos/todo_view_spec.coffeedescribe "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
spec/javascripts/views/todos/todo_view_spec.coffeedescribe "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
app/assets/javascripts/todos/todo_view.coffeeclass OMG.Views.TodoView extends OMG.Views.BaseView
# ...
deleteClicked: (e) => e?.preventDefault() if confirm("Are you sure?") @model.destroy() $(@el).remove()
Monday, February 25, 13
spec/javascripts/views/todos/todo_view_spec.coffeedescribe "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
Monday, February 25, 13
Monday, February 25, 13
sinon.jshttp://sinonjs.org/
Monday, February 25, 13
SINON.JS•spies•stubs•mocks•fake timers•fake XHR•fake servers•more
Monday, February 25, 13
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 dialogKonacha.mochaOptions.ignoreLeaks = true
beforeEach -> @page = $("#konacha") @sandbox = sinon.sandbox.create()
afterEach -> @sandbox.restore()
Monday, February 25, 13
spec/javascripts/views/todos/todo_view_spec.coffeedescribe "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
WHAT ABOUT AJAX REQUESTS?
Monday, February 25, 13
app/assets/javascripts/views/todos/todo_list_view.js.coffeeclass 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
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
Monday, February 25, 13
APPROACH #1MOCK RESPONSES
Monday, February 25, 13
1. DEFINE TEST RESPONSE(S)
Monday, February 25, 13
spec/javascripts/support/mock_responses.coffeewindow.MockServer ?= sinon.fakeServer.create()MockServer.respondWith( "GET", "/todos", [ 200, { "Content-Type": "application/json" }, ''' [ {"body":"Do something!","completed":false,"id":1}, {"body":"Do something else!","completed":false,"id":2} ]''' ])
Monday, February 25, 13
2. RESPOND
Monday, February 25, 13
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
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
Monday, February 25, 13
APPROACH #2 STUBBING
Monday, February 25, 13
spec/javascripts/views/todos/todo_list_view_spec.coffee#= require spec_helper
describe "OMG.Views.TodosListView (Alt.)", -> beforeEach -> @page.html("<ul id='todos'></ul>") @todo1 = new OMG.Models.Todo(id: 1, body: "Do something!") @todo2 = new OMG.Models.Todo(id: 2, body: "Do something else!") @collection = new OMG.Collections.Todos() @sandbox.stub @collection, "fetch", => @collection.add(@todo1, silent: true) @collection.add(@todo2, silent: true) @collection.trigger("reset") @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(new RegExp(@todo1.get("body"))) el.should.match(new RegExp(@todo2.get("body")))
Monday, February 25, 13
Monday, February 25, 13
Monday, February 25, 13
rake konacha:run.........................
Finished in 6.77 seconds25 examples, 0 failures
rake konacha:run SPEC=views/todos/todo_list_view_spec...
Finished in 5.89 seconds3 examples, 0 failures
Monday, February 25, 13
THANK YOU@markbates
http://www.metacasts.tvCONFOO2013
Monday, February 25, 13