75
TESTING RICH *SCRIPT APPLICATIONS WITH RAILS @markbates Monday, February 25, 13

Testing Your JavaScript & CoffeeScript

  • Upload
    mark

  • View
    2.264

  • Download
    1

Embed Size (px)

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.

Citation preview

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