Created
November 24, 2011 22:27
-
-
Save jdudek/1392434 to your computer and use it in GitHub Desktop.
A few thoughts on testing rich-client applications (meet.js)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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