Testing Your JavaScript & CoffeeScript
-
Upload
mark -
Category
Technology
-
view
2.264 -
download
1
description
Transcript of Testing Your JavaScript & CoffeeScript
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