Your SlideShare is downloading. ×
0
TESTING RICH *SCRIPTAPPLICATIONS WITH RAILS         @markbates
Finished in 4.41041 seconds108 examples, 0 failures
A QUICK POLL
app/models/todo.rbclass Todo < ActiveRecord::Base  validates :body, presence: true  attr_accessible :body, :completedend
spec/models/todo_spec.rbrequire spec_helperdescribe Todo do  it "requires a body" do    todo = Todo.new    todo.should_not...
app/controllers/todos_controller.rbclass TodosController < ApplicationController  respond_to :html, :json  def index    re...
spec/controllers/todos_controller_spec.rbrequire spec_helper                                                              ...
app/views/todos/index.html.erb<form class=form-horizontal id=todo_form></form><ul id=todos class="unstyled"></ul><script> ...
SO WHERE’S THE CODE?
app/assets/javascripts/views/todo_view.js.coffeeclass OMG.Views.TodoView extends OMG.Views.BaseView  tagName: li  template...
HOW DO WE TEST THIS?
CAPYBARA?
XCAPYBARA?
Mocha + Chai =
Mocha + Chai =
JavaScript example:describe(panda, function(){  it(is happy, function(){    panda.should.be("happy")  });});              ...
EXPECT/SHOULD/ASSERTexpect(panda).to.be(happy)panda.should.be("happy")assert.equal(panda, happy)expect(foo).to.be.truefoo....
ASSERTIONS/MATCHERS•   to (should)       •   .ok                     •   .instanceof(constructor)•   be                •  ...
MOCHA/CHAI WITH RAILS• gem   konacha• gem   poltergiest (brew install phantomjs)
config/initializers/konacha.rbif defined?(Konacha)  require capybara/poltergeist  Konacha.configure do |config|    config.s...
rake konacha:serve
LET’S WRITE A TEST!
spec/javascripts/spec_helper.coffee# Require the appropriate asset-pipeline files:#= require application# Any other testin...
app/assets/javascript/greeter.js.coffeeclass @Greeter  constructor: (@name) ->    unless @name?      throw new Error("You ...
spec/javascripts/greeter_spec.coffee#= require spec_helperdescribe "Greeter", ->  describe "initialize", ->    it "raises ...
NOW THE HARD STUFF
chai-jqueryhttps://github.com/chaijs/chai-jquery
MATCHERS•   .attr(name[, value])       •   .selected•   .data(name[, value])       •   .checked•   .class(className)      ...
spec/javascripts/spec_helper.coffee# Require the appropriate asset-pipeline files:#= require application#= require_tree ./...
app/assets/javascripts/views/todo_view.js.coffeeclass OMG.Views.TodoView extends OMG.Views.BaseView  tagName: li  template...
spec/javascripts/views/todos/todo_view_spec.coffee#= require spec_helperdescribe "OMG.Views.TodoView", ->  beforeEach ->  ...
spec/javascripts/views/todos/todo_view_spec.coffeedescribe "model bindings", ->  it "re-renders on change", ->    $(.todo-...
spec/javascripts/views/todos/todo_view_spec.coffeedescribe "displaying of todos", ->  it "contains the body of the todo", ...
spec/javascripts/views/todos/todo_view_spec.coffeedescribe "checking the completed checkbox", ->  beforeEach ->    $([name...
app/assets/javascripts/todos/todo_view.coffeeclass OMG.Views.TodoView extends OMG.Views.BaseView  # ...  deleteClicked: (e...
sinon.jshttp://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...
spec/javascripts/views/todos/todo_view_spec.coffeedescribe "clicking the delete button", ->  describe "if confirmed", ->  ...
WHAT ABOUT AJAX  REQUESTS?
app/assets/javascripts/views/todos/todo_list_view.js.coffeeclass OMG.Views.TodosListView extends OMG.Views.BaseView  el: "...
spec/javascripts/views/todos/todo_list_view_spec.coffee#= require spec_helperdescribe "OMG.Views.TodosListView", ->  befor...
APPROACH #1MOCK RESPONSES
1. DEFINE TEST RESPONSE(S)
spec/javascripts/support/mock_responses.coffeewindow.MockServer ?= sinon.fakeServer.create()MockServer.respondWith(  "GET"...
2. RESPOND
spec/javascripts/views/todos/todo_list_view_spec.coffee#= require spec_helperdescribe "OMG.Views.TodosListView", ->  befor...
spec/javascripts/views/todos/todo_list_view_spec.coffee#= require spec_helperdescribe "OMG.Views.TodosListView", ->  befor...
APPROACH #2  STUBBING
spec/javascripts/views/todos/todo_list_view_spec.coffee#= require spec_helperdescribe "OMG.Views.TodosListView (Alt.)", ->...
rake konacha:run.........................Finished in 6.77 seconds25 examples, 0 failuresrake konacha:run SPEC=views/todos/...
THANKS                                            @markbates•   mocha    http://visionmedia.github.com/mocha/•   chai    h...
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Testing JavaScript/CoffeeScript with Mocha and Chai
Upcoming SlideShare
Loading in...5
×

Testing JavaScript/CoffeeScript with Mocha and Chai

12,710

Published on

Presented at the Burlington Ruby Conference on July 28th.

Learn just how easy it is to test your JavaScript and CoffeeScript code. Don't be scared by events, DOM interactions, or AJAX requests any more!

Published in: Technology
0 Comments
22 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
12,710
On Slideshare
0
From Embeds
0
Number of Embeds
1
Actions
Shares
0
Downloads
75
Comments
0
Likes
22
Embeds 0
No embeds

No notes for slide
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • \n
  • Transcript of "Testing JavaScript/CoffeeScript with Mocha and Chai"

    1. 1. TESTING RICH *SCRIPTAPPLICATIONS WITH RAILS @markbates
    2. 2. Finished in 4.41041 seconds108 examples, 0 failures
    3. 3. A QUICK POLL
    4. 4. app/models/todo.rbclass Todo < ActiveRecord::Base validates :body, presence: true attr_accessible :body, :completedend
    5. 5. spec/models/todo_spec.rbrequire spec_helperdescribe Todo do it "requires a body" do todo = Todo.new todo.should_not be_valid todo.errors[:body].should include("cant be blank") todo.body = "Do something" todo.should be_valid endend
    6. 6. 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 endend
    7. 7. spec/controllers/todos_controller_spec.rbrequire 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("cant 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("cant 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
    8. 8. 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>
    9. 9. SO WHERE’S THE CODE?
    10. 10. 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()
    11. 11. HOW DO WE TEST THIS?
    12. 12. CAPYBARA?
    13. 13. XCAPYBARA?
    14. 14. Mocha + Chai =
    15. 15. Mocha + Chai =
    16. 16. JavaScript example:describe(panda, function(){ it(is happy, function(){ panda.should.be("happy") });}); CoffeeScript example:describe panda, -> it is happy, -> panda.should.be("happy")
    17. 17. 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([])
    18. 18. 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)
    19. 19. MOCHA/CHAI WITH RAILS• gem konacha• gem poltergiest (brew install phantomjs)
    20. 20. config/initializers/konacha.rbif defined?(Konacha) require capybara/poltergeist Konacha.configure do |config| config.spec_dir = "spec/javascripts" config.driver = :poltergeist endend
    21. 21. rake konacha:serve
    22. 22. LET’S WRITE A TEST!
    23. 23. 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 = truebeforeEach -> @page = $("#konacha")
    24. 24. app/assets/javascript/greeter.js.coffeeclass @Greeter constructor: (@name) -> unless @name? throw new Error("You need a name!") greet: -> "Hi #{@name}"
    25. 25. spec/javascripts/greeter_spec.coffee#= require spec_helperdescribe "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")
    26. 26. NOW THE HARD STUFF
    27. 27. chai-jqueryhttps://github.com/chaijs/chai-jquery
    28. 28. 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
    29. 29. 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 = truebeforeEach -> @page = $("#konacha")
    30. 30. 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()
    31. 31. spec/javascripts/views/todos/todo_view_spec.coffee#= require spec_helperdescribe "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)
    32. 32. 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!")
    33. 33. 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")
    34. 34. 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")
    35. 35. 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()
    36. 36. sinon.jshttp://sinonjs.org/
    37. 37. SINON.JS• spies• stubs• mocks• fake timers• fake XHR• fake servers• more
    38. 38. 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 = truebeforeEach -> @page = $("#konacha") @sandbox = sinon.sandbox.create()afterEach -> @sandbox.restore()
    39. 39. 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())
    40. 40. WHAT ABOUT AJAX REQUESTS?
    41. 41. 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)
    42. 42. spec/javascripts/views/todos/todo_list_view_spec.coffee#= require spec_helperdescribe "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!/)
    43. 43. APPROACH #1MOCK RESPONSES
    44. 44. 1. DEFINE TEST RESPONSE(S)
    45. 45. 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} ] ])
    46. 46. 2. RESPOND
    47. 47. spec/javascripts/views/todos/todo_list_view_spec.coffee#= require spec_helperdescribe "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!/)
    48. 48. spec/javascripts/views/todos/todo_list_view_spec.coffee#= require spec_helperdescribe "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!/)
    49. 49. APPROACH #2 STUBBING
    50. 50. spec/javascripts/views/todos/todo_list_view_spec.coffee#= require spec_helperdescribe "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")))
    51. 51. rake konacha:run.........................Finished in 6.77 seconds25 examples, 0 failuresrake konacha:run SPEC=views/todos/todo_list_view_spec...Finished in 5.89 seconds3 examples, 0 failures
    52. 52. 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
    1. A particular slide catching your eye?

      Clipping is a handy way to collect important slides you want to go back to later.

    ×