Download - Testing Your JavaScript & CoffeeScript

Transcript
Page 1: Testing Your JavaScript & CoffeeScript

TESTING RICH *SCRIPT APPLICATIONS WITH RAILS

@markbates

Monday, February 25, 13

Page 2: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 3: Testing Your JavaScript & CoffeeScript

http://www.metacasts.tvCONFOO2013

Monday, February 25, 13

Page 4: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 5: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 6: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 7: Testing Your JavaScript & CoffeeScript

Finished in 4.41041 seconds108 examples, 0 failures

Monday, February 25, 13

Page 8: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 9: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 10: Testing Your JavaScript & CoffeeScript

A QUICK POLL

Monday, February 25, 13

Page 11: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 12: Testing Your JavaScript & CoffeeScript

app/models/todo.rbclass Todo < ActiveRecord::Base

validates :body, presence: true

attr_accessible :body, :completed

end

Monday, February 25, 13

Page 13: Testing Your JavaScript & CoffeeScript

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

Page 14: Testing Your JavaScript & CoffeeScript

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

Page 15: Testing Your JavaScript & CoffeeScript

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

Page 16: Testing Your JavaScript & CoffeeScript

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

Page 17: Testing Your JavaScript & CoffeeScript

SO WHERE’S THE CODE?

Monday, February 25, 13

Page 18: Testing Your JavaScript & CoffeeScript

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

Page 19: Testing Your JavaScript & CoffeeScript

HOW DO WE TEST THIS?

Monday, February 25, 13

Page 20: Testing Your JavaScript & CoffeeScript

CAPYBARA?

Monday, February 25, 13

Page 21: Testing Your JavaScript & CoffeeScript

CAPYBARA?XMonday, February 25, 13

Page 22: Testing Your JavaScript & CoffeeScript

Mocha Chai+ =

Monday, February 25, 13

Page 23: Testing Your JavaScript & CoffeeScript

Mocha Chai+ =

Monday, February 25, 13

Page 24: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 25: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 26: Testing Your JavaScript & CoffeeScript

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

Page 27: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 28: Testing Your JavaScript & CoffeeScript

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

Page 29: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 30: Testing Your JavaScript & CoffeeScript

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

Page 31: Testing Your JavaScript & CoffeeScript

MOCHA/CHAI WITH RAILS

• gem 'konacha'

• gem 'poltergiest' (brew install phantomjs)

Monday, February 25, 13

Page 32: Testing Your JavaScript & CoffeeScript

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

Page 33: Testing Your JavaScript & CoffeeScript

rake konacha:serve

Monday, February 25, 13

Page 34: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 35: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 36: Testing Your JavaScript & CoffeeScript

LET’S WRITE A TEST!

Monday, February 25, 13

Page 37: Testing Your JavaScript & CoffeeScript

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

Page 38: Testing Your JavaScript & CoffeeScript

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

Page 39: Testing Your JavaScript & CoffeeScript

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

Page 40: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 41: Testing Your JavaScript & CoffeeScript

NOW THE HARD STUFF

Monday, February 25, 13

Page 42: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 43: Testing Your JavaScript & CoffeeScript

chai-jqueryhttps://github.com/chaijs/chai-jquery

Monday, February 25, 13

Page 44: Testing Your JavaScript & CoffeeScript

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

Page 45: Testing Your JavaScript & CoffeeScript

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

Page 46: Testing Your JavaScript & CoffeeScript

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

Page 47: Testing Your JavaScript & CoffeeScript

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

Page 48: Testing Your JavaScript & CoffeeScript

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

Page 49: Testing Your JavaScript & CoffeeScript

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

Page 50: Testing Your JavaScript & CoffeeScript

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

Page 51: Testing Your JavaScript & CoffeeScript

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

Page 52: Testing Your JavaScript & CoffeeScript

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

Page 53: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 54: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 55: Testing Your JavaScript & CoffeeScript

sinon.jshttp://sinonjs.org/

Monday, February 25, 13

Page 56: Testing Your JavaScript & CoffeeScript

SINON.JS•spies•stubs•mocks•fake timers•fake XHR•fake servers•more

Monday, February 25, 13

Page 57: Testing Your JavaScript & CoffeeScript

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

Page 58: Testing Your JavaScript & CoffeeScript

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

Page 59: Testing Your JavaScript & CoffeeScript

WHAT ABOUT AJAX REQUESTS?

Monday, February 25, 13

Page 60: Testing Your JavaScript & CoffeeScript

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

Page 61: Testing Your JavaScript & CoffeeScript

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

Page 62: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 63: Testing Your JavaScript & CoffeeScript

APPROACH #1MOCK RESPONSES

Monday, February 25, 13

Page 64: Testing Your JavaScript & CoffeeScript

1. DEFINE TEST RESPONSE(S)

Monday, February 25, 13

Page 65: Testing Your JavaScript & CoffeeScript

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

Page 66: Testing Your JavaScript & CoffeeScript

2. RESPOND

Monday, February 25, 13

Page 67: Testing Your JavaScript & CoffeeScript

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

Page 68: Testing Your JavaScript & CoffeeScript

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

Page 69: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 70: Testing Your JavaScript & CoffeeScript

APPROACH #2 STUBBING

Monday, February 25, 13

Page 71: Testing Your JavaScript & CoffeeScript

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

Page 72: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 73: Testing Your JavaScript & CoffeeScript

Monday, February 25, 13

Page 74: Testing Your JavaScript & CoffeeScript

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

Page 75: Testing Your JavaScript & CoffeeScript

THANK YOU@markbates

http://www.metacasts.tvCONFOO2013

Monday, February 25, 13