Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing Realtime web apps with Cucumber.js

Testing Realtime web apps with Cucumber.js

Paul Jensen's presentation from CukeUp 2013, London UK.

Paul Jensen

April 04, 2013
Tweet

More Decks by Paul Jensen

Other Decks in Technology

Transcript

  1. About me I work at Axisto Media I’m a Ruby

    on Rails / Node.js developer I’m a Core contributor to SocketStream
  2. A few weeks later, I decided I wanted to prove

    that I wasn’t a crap developer.
  3. :)

  4. : o

  5. - The app is a single page app - Client-Server

    interaction happens over the WebSocket protocol. - The client-side framework is custom. - The tech stack is bleeding edge.
  6. We could write tests like this Feature: login In order

    to use the site As a user I want to login Scenario: Login (with username) Given a user exists with username "paulbjensen" and email "[email protected]" and password "123456" And a dashboard exists with name "Your Dashboard" for user "paulbjensen" And I am on the homepage And I follow "Login" And the "login" modal should appear And I fill in "identifier" with "paulbjensen" And I fill in "password" with "123456" And I press "Login" Then the "login" modal should disappear And I should be on the dashboard page
  7. We would then need to hook up the Cucumber steps

    to... - Seed/Clean the database with data - Open a web browser - Click buttons, fill in text fields - Check that the app did what we expected
  8. An example of Soda’s API browser .chain .session() .open('/') .type('q',

    'Hello World') .end(function(err){ browser.testComplete(function() { console.log('done'); if(err) throw err; }); });
  9. features/support/world.coffee selenium = require 'selenium-launcher' soda = require 'soda' #

    We want to spin up the selenium server & client once browser = null World = (callback) -> if browser is null # Boot the selenium server selenium (err, selenium) => # Boot the selenium client browser = soda.createClient host: selenium.host port: selenium.port url: "http://localhost:3000" browser: "firefox"
  10. features/support/world.coffee # Make Soda's browser object available to # the

    step definitions @browser = browser callback {@browser} # Kill the selenium server when the process ends process.on 'exit', -> selenium.kill() else # Make Soda's browser object available to # the step definitions @browser = browser callback {@browser} exports.World = World
  11. features/step_definitions/myStepDefinitions.coffee # A helper function to DRY up calls to

    Soda's API wrap = (funk, cb) -> funk.end (err) -> if err? cb.fail err else cb() module.exports = -> @World = require("../support/world.coffee").World @Given /^I am on the homepage$/, (callback) -> wrap @browser.chain.session().open('/'), callback
  12. features/support/world.coffee selenium = require 'selenium-launcher' soda = require 'soda' process.env["SS_ENV"]

    = "cucumber" ss = require 'socketstream' config = require '../../server/config.coffee' app = require '../../app.coffee' browser = null World = (callback) ->
  13. server/internals.coffee # Internals, where all the config is loaded and

    set into the application ss = require 'socketstream' app = models : {} controllers : {} schemas : {} helpers : {} app.config = require('./config')[ss.env] require("./db") app require("./helpers") app require("./controllers/dashboard") app require("./controllers/widget") app ss.api.add 'app', app
  14. server/db.coffee # npm modules mongoose = require 'mongoose' redis =

    require 'redis' ss = require 'socketstream' module.exports = (app) -> # Redis-related configuration app.Redis = redis.createClient app.config.redis.port, app.config.redis.host app.Redis.auth(app.config.redis.pass) if ss.env is 'production' # MongoDB-related configuration mongoose.connect "mongodb://#{app.config.db}" require("#{__dirname}/models/user.coffee") app require("#{__dirname}/models/widget.coffee") app require("#{__dirname}/models/dashboard.coffee") app require("#{__dirname}/models/widgetTemplate.coffee") app
  15. server/models/widgetTemplate.coffee #### WidgetTemplate model #### mongoose = require 'mongoose' module.exports

    = (app) -> app.schemas.WidgetTemplates = new mongoose.Schema name : type: String html : type: String css : type: String script : type: String scriptType : type: String, default: 'javascript' json : type: String snapshotUrl : type: String width : type: Number, default: 200 height : type: Number, default: 180 createdAt : type: Date, default: Date.now updatedAt : type: Date, default: Date.now WidgetTemplate = mongoose.model 'WidgetTemplate', app.schemas.WidgetTemplates app.models.WidgetTemplate = WidgetTemplate
  16. features/stepDefinitions/myStepDefinitions.coffee fs = require "fs" ss = require 'socketstream' User

    = ss.api.app.models.User Dashboard = ss.api.app.models.Dashboard WidgetTemplate = ss.api.app.models.WidgetTemplate widgetController = ss.api.app.controllers.widget module.exports = -> @World = require("../support/world.coffee").World @Before (callback) -> User.remove {}, (err) -> Dashboard.remove {}, (err) -> WidgetTemplate.remove {}, (err) -> callback()
  17. features/stepDefinitions/myStepDefinitions.coffee @Given /^a user exists with username "([^"]*)" and email

    "([^"]*)" and password "([^"]*)"$/, (username, email, password, callback) -> user = new User {username, email, password} user.save (err, doc) -> if err? callback.fail err else callback()
  18. features/stepDefinitions/myStepDefinitions.coffee @Given /^there should be a user with username "([^"]*)"$/,

    (username, callback) -> User.find {username}, (err, docs) -> if !err? and docs.length is 1 callback() else callback.fail "Expected there to be 1 user record with username #{username}, but found #{docs.length}"
  19. There was no magic bullet - you just have to

    stitch the libraries together
  20. 1 - Whatever you do, put this in your Step

    Definitions file @After (callback) -> wrap @browser.chain.testComplete(), callback
  21. 2 - waitForElementPresent is your friend @Given /^I click on

    the "([^"]*)" button$/, (name, callback) -> detectButton name, (selector) => wrap @browser .chain .waitForElementPresent(selector) .click(selector) , callback
  22. 3 - Master XPATH selectors and the selenium commands reference

    @Then /^there should be an "([^"]*)" item in the Dashboards menu list$/, (item, callback) -> wrap @browser.chain.assertElementPresent("//span[contains(text(),'#{item}')]"), callback
  23. .travis.yml language: node_js node_js: - 0.8 services: - mongodb -

    redis-server before_install: - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" script: npm test && npm run-script cuke
  24. Also, if you’re thinking of building a SocketStream app, and

    would like to use Cucumber.js with it...
  25. “- Everything around you that you call life was made

    up by people that were no smarter than you ... and you can change it, you can influence it, you can build your own things that other people can use. Once you learn that, you'll never be the same again." Steve Jobs