Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
A few thoughts on testing rich-client applications (meet.js)
# A few thoughts on testing rich-client applications
# Jan Dudek, Arkency
# We've created a game that has some animations, uses Facebook API. Almost no rendering on the server. Client-server communication through JSON REST API.
# First try: Selenium tests.
# Slow, unreliable. Running tests quickly begins to take minutes.
# Hard to test animation, time-dependent actions, race conditions.
# Does not cover API very well. (Need to test server-side anyway).
# We decided to have separate tests for client and server.
# A few Selenium scenarios remain, but I hate them.
# App architecture:
# Do-it-yourself MVC (no Backbone, Spine, etc.)
# Mostly "plain old objects"
# Dependency injection almost everywhere
# One class ("Application") that wires all objects together
# Jasmine as unit and acceptance tests
# What makes testing hard? External actors.
# Server-side API
# Facebook API
# Time
# AJAX calls abstracted to ServerSide class
# Easy to mock/replace in tests
# Easy to control order of server responses - allows testing race conditions. (Probably impossible in Selenium).
# Facebook SDK - ugly, everything is global
#
# replaced with FacebookAdapter, easy to mock in tests
# Clock object - easily test waiting for something to happen.
#
# Triggers "tick" event every 10ms (using window.setInterval)
# In tests replaced by fake object that can be told to move time forward
# e.g. clock.advance(miliseconds: 300)
# Also provides replacement for window.setTimeout - clock.setTimeout
# clock.setTimeout has inverted order of arguments. window.setTimeout was so annoying!
# Example unit test
describe "Countdown", ->
beforeEach ->
@clock = new TestClock
@countdown = new Countdown(@clock, 5)
@updateObserver = jasmine.createSpy("updateObserver")
@finishObserver = jasmine.createSpy("finishObserver")
@countdown.bind("updated", @updateObserver)
@countdown.bind("finished", @finishObserver)
@countdown.start()
describe "after 1 second", ->
beforeEach ->
@clock.advance(seconds: 1)
it "should update to 4 seconds", ->
expect(@updateObserver).toHaveBeenCalledWith(4)
describe "after 5 seconds", ->
beforeEach ->
@clock.advance(seconds: 5)
it "should update with each second", ->
expect(@updateObserver).toHaveBeenCalledWith(4)
expect(@updateObserver).toHaveBeenCalledWith(3)
expect(@updateObserver).toHaveBeenCalledWith(2)
expect(@updateObserver).toHaveBeenCalledWith(1)
expect(@updateObserver).toHaveBeenCalledWith(0)
it "should trigger 'finished' event", ->
expect(@finishObserver).toHaveBeenCalled()
# Example acceptance test:
beforeEach ->
# ...
@app = new Application(@facebook, @server, @audioPlayer, @clock)
@app.start()
scenario "player enters the game", ->
expect($("#logo")).toBeVisible()
expect($("#throbber")).toBeVisible()
@server.respondTo("GET /player", { name: "John Doe", accepted_rules: false })
@facebook.finishInitialization()
@audioPlayer.finishInitialization()
expect($("#throbber")).not.toBeVisible()
expect(@facebook.setFrameSize).toHaveBeenCalled()
expect($("#status .name")).toHaveText("John Doe")
$("#game_screen a.play").click()
@server.respondTo("postGameSession", { ... })
# and so on...
# Benefits of this approach
# Much, much faster than Selenium - 3-5 seconds instead of minutes
# Easy control of time and mocking external services (possible in Selenium via execute_script, but very hacky)
# Thanks to mocking clock/external calls - only synchronous code in tests. Big win.
# Testing race conditions
# Issues
# We have to manually ensure that API is used correctly :(
# Only single-page apps
# Still undecided how to structure acceptance tests in Jasmine (TestUser object? long or short scenarios?)
# Dependency injection - helpful, but Application class becomes messy; some classes with a dozen parameters in constructor
# Thanks!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.