Slide 1

Slide 1 text

Testing Realtime web apps with Cucumber.js Paul Jensen

Slide 2

Slide 2 text

About me I work at Axisto Media I’m a Ruby on Rails / Node.js developer I’m a Core contributor to SocketStream

Slide 3

Slide 3 text

April 2012

Slide 4

Slide 4 text

I was fired from a startup

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

It felt like a death sentence on my career

Slide 7

Slide 7 text

A few weeks later, I decided I wanted to prove that I wasn’t a crap developer.

Slide 8

Slide 8 text

So I built an app called Dashku

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

July 2012

Slide 11

Slide 11 text

I posted Dashku to Hacker News

Slide 12

Slide 12 text

About a week later, Chris Matthieu at Bechtel got in touch

Slide 13

Slide 13 text

“Hi, we’d like to use Dashku within our company”

Slide 14

Slide 14 text

“Sure, but there’s a small thing...”

Slide 15

Slide 15 text

“...it’s a 2 month prototype. It has no tests!”

Slide 16

Slide 16 text

“We’d like to use it anyway”

Slide 17

Slide 17 text

:)

Slide 18

Slide 18 text

: o

Slide 19

Slide 19 text

*Truth is I needed money, so I offered to write a test suite for Dashku.

Slide 20

Slide 20 text

The road to testing Dashku

Slide 21

Slide 21 text

Dashku was powered by C ff S r p SockJS

Slide 22

Slide 22 text

The client was written in... A custom stack

Slide 23

Slide 23 text

The client was written in... A custom stack ✔ x x x

Slide 24

Slide 24 text

- 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.

Slide 25

Slide 25 text

How do you test this application?

Slide 26

Slide 26 text

It had to be in the browser

Slide 27

Slide 27 text

In the past at AOL, we kind of did this

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

We tested this app using Ruby & RSpec to drive a web browser

Slide 30

Slide 30 text

This was a bit like...

Slide 31

Slide 31 text

No content

Slide 32

Slide 32 text

It worked, but it wasn’t ideal (we seeded data outside of business logic)

Slide 33

Slide 33 text

For Dashku, I wanted to use Cucumber, & I wanted to test it all in Node.js

Slide 34

Slide 34 text

I wanted to be like...

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

We needed a tool to run Cucumber features in Node.js

Slide 37

Slide 37 text

No content

Slide 38

Slide 38 text

Cucumber.js was the most important part of our test suite

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

We then needed a tool to power a web browser

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

Soda & Selenium Launcher could power a web browser via Selenium and Node.js

Slide 45

Slide 45 text

How could we make Cucumber.js work with Soda and Selenium Launcher?

Slide 46

Slide 46 text

Start with Cucumber.js’ world file

Slide 47

Slide 47 text

features/support/world.coffee World = (callback) -> callback {} exports.World = World

Slide 48

Slide 48 text

Make the World file available to your Step Definitions

Slide 49

Slide 49 text

features/step_definitions/myStepDefinitions.coffee module.exports = -> @World = require("../support/world.coffee").World

Slide 50

Slide 50 text

Let’s find a way to power the web browser via the step definitions

Slide 51

Slide 51 text

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; }); });

Slide 52

Slide 52 text

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"

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

With this in place, we can power the web browser from our Cucumber steps

Slide 56

Slide 56 text

We then needed to boot the SocketStream application, and seed the database

Slide 57

Slide 57 text

To boot SocketStream, simply load the file where the app is booted.

Slide 58

Slide 58 text

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) ->

Slide 59

Slide 59 text

To Seed/Clear the database, expose your models in the Step definitions file

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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()

Slide 64

Slide 64 text

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()

Slide 65

Slide 65 text

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}"

Slide 66

Slide 66 text

There was no magic bullet - you just have to stitch the libraries together

Slide 67

Slide 67 text

But once you stitch them together, it feels like magic

Slide 68

Slide 68 text

Let’s see it in action

Slide 69

Slide 69 text

Tips for using Soda

Slide 70

Slide 70 text

1 - Whatever you do, put this in your Step Definitions file @After (callback) -> wrap @browser.chain.testComplete(), callback

Slide 71

Slide 71 text

... or this will happen.

Slide 72

Slide 72 text

2 - waitForElementPresent is your friend @Given /^I click on the "([^"]*)" button$/, (name, callback) -> detectButton name, (selector) => wrap @browser .chain .waitForElementPresent(selector) .click(selector) , callback

Slide 73

Slide 73 text

Use waitFor to delay executing the next chained call, until a condition is met

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

http:// release.seleniumhq.org/ selenium-core/1.0.1/ reference.html

Slide 76

Slide 76 text

Other tips

Slide 77

Slide 77 text

You can use Travis to run your cucumber.js tests

Slide 78

Slide 78 text

No content

Slide 79

Slide 79 text

.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

Slide 80

Slide 80 text

github.com/anephenix/dashku

Slide 81

Slide 81 text

Also, if you’re thinking of building a SocketStream app, and would like to use Cucumber.js with it...

Slide 82

Slide 82 text

github.com/anephenix/ss-cucumber

Slide 83

Slide 83 text

ss-cucumber will generate files and folders to help you use Cucumber.js with SocketStream

Slide 84

Slide 84 text

Example npm install -g ss-cucumber cd ss-cucumber init

Slide 85

Slide 85 text

Final Thoughts

Slide 86

Slide 86 text

If I wasn’t fired a year ago, none of this would exist.

Slide 87

Slide 87 text

Bleeding-edge Realtime-web apps can be tested using Cucumber!

Slide 88

Slide 88 text

“- 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

Slide 89

Slide 89 text

Thank You @paulbjensen