Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # 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!
Add Comment
Please, Sign In to add comment