TESTING RICH *SCRIPT
APPLICATIONS WITH RAILS
         @markbates
Finished in 4.41041 seconds
108 examples, 0 failures
A QUICK POLL
app/models/todo.rb
class Todo < ActiveRecord::Base

  validates :body, presence: true

  attr_accessible :body, :completed

end
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
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
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
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>
SO WHERE’S THE CODE?
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()
HOW DO WE TEST THIS?
CAPYBARA?
X
CAPYBARA?
Mocha + Chai =
Mocha + Chai =
JavaScript example:
describe('panda', function(){
  it('is happy', function(){
    panda.should.be("happy")
  });
});



                CoffeeScript example:
describe 'panda', ->
  it 'is happy', ->
    panda.should.be("happy")
EXPECT/SHOULD/ASSERT
expect(panda).to.be('happy')
panda.should.be("happy")
assert.equal(panda, 'happy')

expect(foo).to.be.true
foo.should.be.true
assert.isTrue(foo)

expect(foo).to.be.null
foo.should.be.null
assert.isNull(foo)

expect([]).to.be.empty
[].should.be.empty
assert.isEmpty([])
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)
MOCHA/CHAI WITH RAILS

• gem   'konacha'

• gem   'poltergiest' (brew install phantomjs)
config/initializers/konacha.rb
if defined?(Konacha)
  require 'capybara/poltergeist'
  Konacha.configure do |config|
    config.spec_dir = "spec/javascripts"
    config.driver    = :poltergeist
  end
end
rake konacha:serve
LET’S WRITE A TEST!
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")
app/assets/javascript/greeter.js.coffee
class @Greeter

  constructor: (@name) ->
    unless @name?
      throw new Error("You need a name!")

  greet: ->
    "Hi #{@name}"
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")
NOW THE HARD STUFF
chai-jquery
https://github.com/chaijs/chai-jquery
MATCHERS
•   .attr(name[, value])       •   .selected
•   .data(name[, value])       •   .checked
•   .class(className)          •   .disabled
•   .id(id)                    •   .exist
•   .html(html)                •   .match(selector) / .be(selector)
•   .text(text)                •   .contain(selector)
•   .value(value)              •   .have(selector)
•   .visible
•   .hidden
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")
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()
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)
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!")
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")
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")
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()
sinon.js
http://sinonjs.org/
SINON.JS
• spies
• stubs
• mocks
• fake   timers
• fake   XHR
• fake   servers
• more
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()
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())
WHAT ABOUT AJAX
  REQUESTS?
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)
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!/)
APPROACH #1
MOCK RESPONSES
1. DEFINE TEST RESPONSE(S)
spec/javascripts/support/mock_responses.coffee
window.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}
    ]'''
  ]
)
2. RESPOND
spec/javascripts/views/todos/todo_list_view_spec.coffee
#= require spec_helper

describe "OMG.Views.TodosListView", ->

  beforeEach ->
    page.html(template("todos"))
    @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!/)
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!/)
APPROACH #2
  STUBBING
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")))
rake konacha:run
.........................

Finished in 6.77 seconds
25 examples, 0 failures




rake konacha:run SPEC=views/todos/todo_list_view_spec
...

Finished in 5.89 seconds
3 examples, 0 failures
THANKS
                                            @markbates
•   mocha
    http://visionmedia.github.com/mocha/

•   chai
    http://chaijs.com/

•   konacha
    https://github.com/jfirebaugh/konacha

•   chai-jquery
    https://github.com/chaijs/chai-jquery

•   sinon.js
    http://sinonjs.org/

•   poltergiest
    https://github.com/jonleighton/poltergeist

•   phantom.js
    http://phantomjs.org/

•   Programming in CoffeeScript
    http://books.markbates.com

Testing JavaScript/CoffeeScript with Mocha and Chai

  • 1.
    TESTING RICH *SCRIPT APPLICATIONSWITH RAILS @markbates
  • 5.
    Finished in 4.41041seconds 108 examples, 0 failures
  • 8.
  • 9.
    app/models/todo.rb class Todo <ActiveRecord::Base validates :body, presence: true attr_accessible :body, :completed end
  • 10.
    spec/models/todo_spec.rb require 'spec_helper' describe Tododo 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
  • 11.
    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
  • 12.
    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
  • 13.
    app/views/todos/index.html.erb <form class='form-horizontal' id='todo_form'></form> <ulid='todos' class="unstyled"></ul> <script> $(function() { new OMG.Views.TodosApp(); }) </script>
  • 14.
  • 15.
    app/assets/javascripts/views/todo_view.js.coffee class OMG.Views.TodoView extendsOMG.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()
  • 16.
    HOW DO WETEST THIS?
  • 17.
  • 18.
  • 19.
  • 20.
  • 23.
    JavaScript example: describe('panda', function(){ it('is happy', function(){ panda.should.be("happy") }); }); CoffeeScript example: describe 'panda', -> it 'is happy', -> panda.should.be("happy")
  • 25.
  • 27.
    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)
  • 28.
    MOCHA/CHAI WITH RAILS •gem 'konacha' • gem 'poltergiest' (brew install phantomjs)
  • 29.
    config/initializers/konacha.rb if defined?(Konacha) require 'capybara/poltergeist' Konacha.configure do |config| config.spec_dir = "spec/javascripts" config.driver = :poltergeist end end
  • 30.
  • 33.
  • 34.
    spec/javascripts/spec_helper.coffee # Require theappropriate 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")
  • 35.
    app/assets/javascript/greeter.js.coffee class @Greeter constructor: (@name) -> unless @name? throw new Error("You need a name!") greet: -> "Hi #{@name}"
  • 36.
    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")
  • 38.
  • 40.
  • 41.
    MATCHERS • .attr(name[, value]) • .selected • .data(name[, value]) • .checked • .class(className) • .disabled • .id(id) • .exist • .html(html) • .match(selector) / .be(selector) • .text(text) • .contain(selector) • .value(value) • .have(selector) • .visible • .hidden
  • 42.
    spec/javascripts/spec_helper.coffee # Require theappropriate 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")
  • 43.
    app/assets/javascripts/views/todo_view.js.coffee class OMG.Views.TodoView extendsOMG.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()
  • 44.
    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)
  • 45.
    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!")
  • 46.
    spec/javascripts/views/todos/todo_view_spec.coffee describe "displaying oftodos", -> 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")
  • 47.
    spec/javascripts/views/todos/todo_view_spec.coffee describe "checking thecompleted 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")
  • 48.
    app/assets/javascripts/todos/todo_view.coffee class OMG.Views.TodoView extendsOMG.Views.BaseView # ... deleteClicked: (e) => e?.preventDefault() if confirm("Are you sure?") @model.destroy() $(@el).remove()
  • 51.
  • 52.
    SINON.JS • spies • stubs •mocks • fake timers • fake XHR • fake servers • more
  • 53.
    spec/javascripts/spec_helper.coffee # Require theappropriate 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()
  • 54.
    spec/javascripts/views/todos/todo_view_spec.coffee describe "clicking thedelete 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())
  • 55.
  • 56.
    app/assets/javascripts/views/todos/todo_list_view.js.coffee class OMG.Views.TodosListView extendsOMG.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)
  • 57.
    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!/)
  • 59.
  • 60.
    1. DEFINE TESTRESPONSE(S)
  • 61.
    spec/javascripts/support/mock_responses.coffee window.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} ]''' ] )
  • 62.
  • 63.
    spec/javascripts/views/todos/todo_list_view_spec.coffee #= require spec_helper describe"OMG.Views.TodosListView", -> beforeEach -> page.html(template("todos")) @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!/)
  • 64.
    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!/)
  • 66.
    APPROACH #2 STUBBING
  • 67.
    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")))
  • 70.
    rake konacha:run ......................... Finished in6.77 seconds 25 examples, 0 failures rake konacha:run SPEC=views/todos/todo_list_view_spec ... Finished in 5.89 seconds 3 examples, 0 failures
  • 71.
    THANKS @markbates • mocha http://visionmedia.github.com/mocha/ • chai http://chaijs.com/ • konacha https://github.com/jfirebaugh/konacha • chai-jquery https://github.com/chaijs/chai-jquery • sinon.js http://sinonjs.org/ • poltergiest https://github.com/jonleighton/poltergeist • phantom.js http://phantomjs.org/ • Programming in CoffeeScript http://books.markbates.com