0
TESTING RICH *SCRIPT             APPLICATIONS WITH RAILS                          @markbatesMonday, February 25, 13
Monday, February 25, 13
http://www.metacasts.tv                             CONFOO2013Monday, February 25, 13
Monday, February 25, 13
Monday, February 25, 13
Monday, February 25, 13
Finished in 4.41041 seconds    108 examples, 0 failuresMonday, February 25, 13
Monday, February 25, 13
Monday, February 25, 13
A QUICK POLLMonday, February 25, 13
Monday, February 25, 13
app/models/todo.rb   class Todo < ActiveRecord::Base       validates :body, presence: true       attr_accessible :body, :c...
spec/models/todo_spec.rb   require spec_helper   describe Todo do       it "requires a body" do         todo = Todo.new   ...
app/controllers/todos_controller.rb   class TodosController < ApplicationController     respond_to :html, :json      def i...
spec/controllers/todos_controller_spec.rb   require spec_helper                                                           ...
app/views/todos/index.html.erb   <form class=form-horizontal id=todo_form></form>   <ul id=todos class="unstyled"></ul>   ...
SO WHERE’S THE CODE?Monday, February 25, 13
app/assets/javascripts/views/todo_view.js.coffee        class OMG.Views.TodoView extends OMG.Views.BaseView           tagN...
HOW DO WE TEST THIS?Monday, February 25, 13
CAPYBARA?Monday, February 25, 13
X                          CAPYBARA?Monday, 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:   describe(panda, function(){     it(is happy, function(){       panda.should.be("happy")     });   })...
Monday, February 25, 13
EXPECT/SHOULD/ASSERT           expect(panda).to.be(happy)           panda.should.be("happy")           assert.equal(panda,...
Monday, February 25, 13
ASSERTIONS/MATCHERS    •   to (should)       •   .ok                     •   .instanceof(constructor)    •   be           ...
MOCHA/CHAI WITH RAILS    • gem           konacha    • gem           poltergiest (brew install phantomjs)Monday, February 2...
config/initializers/konacha.rb   if defined?(Konacha)     require capybara/poltergeist     Konacha.configure do |config|   ...
rake konacha:serveMonday, 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 oth...
app/assets/javascript/greeter.js.coffee   class @Greeter       constructor: (@name) ->         unless @name?           thr...
spec/javascripts/greeter_spec.coffee   #= require spec_helper   describe "Greeter", ->       describe "initialize", ->    ...
Monday, February 25, 13
NOW THE HARD STUFFMonday, February 25, 13
Monday, February 25, 13
chai-jquery                          https://github.com/chaijs/chai-jqueryMonday, February 25, 13
MATCHERS    •   .attr(name[, value])       •   .selected    •   .data(name[, value])       •   .checked    •   .class(clas...
spec/javascripts/spec_helper.coffee   # Require the appropriate asset-pipeline files:   #= require application   #= requir...
app/assets/javascripts/views/todo_view.js.coffee   class OMG.Views.TodoView extends OMG.Views.BaseView       tagName: li  ...
spec/javascripts/views/todos/todo_view_spec.coffee   #= require spec_helper   describe "OMG.Views.TodoView", ->       befo...
spec/javascripts/views/todos/todo_view_spec.coffee   describe "model bindings", ->       it "re-renders on change", ->    ...
spec/javascripts/views/todos/todo_view_spec.coffee   describe "displaying of todos", ->       it "contains the body of the...
spec/javascripts/views/todos/todo_view_spec.coffee   describe "checking the completed checkbox", ->       beforeEach ->   ...
app/assets/javascripts/todos/todo_view.coffee   class OMG.Views.TodoView extends OMG.Views.BaseView       # ...       dele...
spec/javascripts/views/todos/todo_view_spec.coffee   describe "clicking the delete button", ->       describe "if confirme...
Monday, February 25, 13
Monday, February 25, 13
sinon.js                          http://sinonjs.org/Monday, February 25, 13
SINON.JS    • spies    • stubs    • mocks    • fake                timers    • fake                XHR    • fake          ...
spec/javascripts/spec_helper.coffee   # Require the appropriate asset-pipeline files:   #= require application   #= requir...
spec/javascripts/views/todos/todo_view_spec.coffee   describe "clicking the delete button", ->       describe "if confirme...
WHAT ABOUT AJAX                            REQUESTS?Monday, February 25, 13
app/assets/javascripts/views/todos/todo_list_view.js.coffee   class OMG.Views.TodosListView extends OMG.Views.BaseView    ...
spec/javascripts/views/todos/todo_list_view_spec.coffee   #= require spec_helper   describe "OMG.Views.TodosListView", -> ...
Monday, February 25, 13
APPROACH #1                          MOCK RESPONSESMonday, February 25, 13
1. DEFINE TEST RESPONSE(S)Monday, February 25, 13
spec/javascripts/support/mock_responses.coffee   window.MockServer ?= sinon.fakeServer.create()   MockServer.respondWith( ...
2. RESPONDMonday, February 25, 13
spec/javascripts/views/todos/todo_list_view_spec.coffee   #= require spec_helper   describe "OMG.Views.TodosListView", -> ...
spec/javascripts/views/todos/todo_list_view_spec.coffee   #= require spec_helper   describe "OMG.Views.TodosListView", -> ...
Monday, February 25, 13
APPROACH #2                            STUBBINGMonday, February 25, 13
spec/javascripts/views/todos/todo_list_view_spec.coffee   #= require spec_helper   describe "OMG.Views.TodosListView (Alt....
Monday, February 25, 13
Monday, February 25, 13
rake konacha:run   .........................   Finished in 6.77 seconds   25 examples, 0 failures    rake konacha:run SPEC...
THANK YOU                             @markbates                          http://www.metacasts.tv                         ...
Upcoming SlideShare
Loading in...5
×

Testing Your JavaScript & CoffeeScript

2,313

Published on

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.

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

No Downloads
Views
Total Views
2,313
On Slideshare
0
From Embeds
0
Number of Embeds
3
Actions
Shares
0
Downloads
23
Comments
0
Likes
3
Embeds 0
No embeds

No notes for slide

Transcript of "Testing Your JavaScript & CoffeeScript"

  1. 1. TESTING RICH *SCRIPT APPLICATIONS WITH RAILS @markbatesMonday, February 25, 13
  2. 2. Monday, February 25, 13
  3. 3. http://www.metacasts.tv CONFOO2013Monday, February 25, 13
  4. 4. Monday, February 25, 13
  5. 5. Monday, February 25, 13
  6. 6. Monday, February 25, 13
  7. 7. Finished in 4.41041 seconds 108 examples, 0 failuresMonday, February 25, 13
  8. 8. Monday, February 25, 13
  9. 9. Monday, February 25, 13
  10. 10. A QUICK POLLMonday, February 25, 13
  11. 11. Monday, February 25, 13
  12. 12. app/models/todo.rb class Todo < ActiveRecord::Base validates :body, presence: true attr_accessible :body, :completed endMonday, February 25, 13
  13. 13. spec/models/todo_spec.rb require spec_helper describe 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 end endMonday, February 25, 13
  14. 14. app/controllers/todos_controller.rb class 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 endMonday, February 25, 13
  15. 15. spec/controllers/todos_controller_spec.rb require 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) endMonday, February 25, 13
  16. 16. 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
  17. 17. SO WHERE’S THE CODE?Monday, February 25, 13
  18. 18. app/assets/javascripts/views/todo_view.js.coffee class 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
  19. 19. HOW DO WE TEST THIS?Monday, February 25, 13
  20. 20. CAPYBARA?Monday, February 25, 13
  21. 21. X CAPYBARA?Monday, February 25, 13
  22. 22. Mocha + Chai =Monday, February 25, 13
  23. 23. Mocha + Chai =Monday, February 25, 13
  24. 24. Monday, February 25, 13
  25. 25. Monday, February 25, 13
  26. 26. JavaScript example: describe(panda, function(){ it(is happy, function(){ panda.should.be("happy") }); }); CoffeeScript example: describe panda, -> it is happy, -> panda.should.be("happy")Monday, February 25, 13
  27. 27. Monday, February 25, 13
  28. 28. EXPECT/SHOULD/ASSERT expect(panda).to.be(happy) panda.should.be("happy") assert.equal(panda, happy) expect(foo).to.be.true foo.should.be.true assert.isTrue(foo) expect(foo).to.be.null foo.should.be.null assert.isNull(foo) expect([]).to.be.empty [].should.be.empty assert.isEmpty([])Monday, February 25, 13
  29. 29. Monday, February 25, 13
  30. 30. 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)Monday, February 25, 13
  31. 31. MOCHA/CHAI WITH RAILS • gem konacha • gem poltergiest (brew install phantomjs)Monday, February 25, 13
  32. 32. config/initializers/konacha.rb if defined?(Konacha) require capybara/poltergeist Konacha.configure do |config| config.spec_dir = "spec/javascripts" config.driver = :poltergeist end endMonday, February 25, 13
  33. 33. rake konacha:serveMonday, February 25, 13
  34. 34. Monday, February 25, 13
  35. 35. Monday, February 25, 13
  36. 36. LET’S WRITE A TEST!Monday, February 25, 13
  37. 37. 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 dialog Konacha.mochaOptions.ignoreLeaks = true beforeEach -> @page = $("#konacha")Monday, February 25, 13
  38. 38. app/assets/javascript/greeter.js.coffee class @Greeter constructor: (@name) -> unless @name? throw new Error("You need a name!") greet: -> "Hi #{@name}"Monday, February 25, 13
  39. 39. 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
  40. 40. Monday, February 25, 13
  41. 41. NOW THE HARD STUFFMonday, February 25, 13
  42. 42. Monday, February 25, 13
  43. 43. chai-jquery https://github.com/chaijs/chai-jqueryMonday, February 25, 13
  44. 44. 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 • .hiddenMonday, February 25, 13
  45. 45. 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 dialog Konacha.mochaOptions.ignoreLeaks = true beforeEach -> @page = $("#konacha")Monday, February 25, 13
  46. 46. app/assets/javascripts/views/todo_view.js.coffee class 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
  47. 47. 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
  48. 48. spec/javascripts/views/todos/todo_view_spec.coffee describe "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
  49. 49. spec/javascripts/views/todos/todo_view_spec.coffee describe "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
  50. 50. spec/javascripts/views/todos/todo_view_spec.coffee describe "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
  51. 51. app/assets/javascripts/todos/todo_view.coffee class OMG.Views.TodoView extends OMG.Views.BaseView # ... deleteClicked: (e) => e?.preventDefault() if confirm("Are you sure?") @model.destroy() $(@el).remove()Monday, February 25, 13
  52. 52. spec/javascripts/views/todos/todo_view_spec.coffee describe "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
  53. 53. Monday, February 25, 13
  54. 54. Monday, February 25, 13
  55. 55. sinon.js http://sinonjs.org/Monday, February 25, 13
  56. 56. SINON.JS • spies • stubs • mocks • fake timers • fake XHR • fake servers • moreMonday, February 25, 13
  57. 57. 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 dialog Konacha.mochaOptions.ignoreLeaks = true beforeEach -> @page = $("#konacha") @sandbox = sinon.sandbox.create() afterEach -> @sandbox.restore()Monday, February 25, 13
  58. 58. spec/javascripts/views/todos/todo_view_spec.coffee describe "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
  59. 59. WHAT ABOUT AJAX REQUESTS?Monday, February 25, 13
  60. 60. app/assets/javascripts/views/todos/todo_list_view.js.coffee class 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
  61. 61. 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
  62. 62. Monday, February 25, 13
  63. 63. APPROACH #1 MOCK RESPONSESMonday, February 25, 13
  64. 64. 1. DEFINE TEST RESPONSE(S)Monday, February 25, 13
  65. 65. spec/javascripts/support/mock_responses.coffee window.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
  66. 66. 2. RESPONDMonday, February 25, 13
  67. 67. 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
  68. 68. 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
  69. 69. Monday, February 25, 13
  70. 70. APPROACH #2 STUBBINGMonday, February 25, 13
  71. 71. 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
  72. 72. Monday, February 25, 13
  73. 73. Monday, February 25, 13
  74. 74. rake konacha:run ......................... Finished in 6.77 seconds 25 examples, 0 failures rake konacha:run SPEC=views/todos/todo_list_view_spec ... Finished in 5.89 seconds 3 examples, 0 failuresMonday, February 25, 13
  75. 75. THANK YOU @markbates http://www.metacasts.tv CONFOO2013Monday, February 25, 13
  1. A particular slide catching your eye?

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

×