Testing Your JavaScript & CoffeeScript

Preview:

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

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

Recommended