Skip to content

Instantly share code, notes, and snippets.

@doitian
Created March 12, 2012 19:33
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save doitian/2024197 to your computer and use it in GitHub Desktop.
Save doitian/2024197 to your computer and use it in GitHub Desktop.
3 ways to test ajax requests or other asynchronouse lib based on jquery deferred
# The second way is recommended, which is flexible, and also work for non jQuery ajax library.
# Setup sinon sandbox
beforeEach ->
@sinon = sinon.sandbox.create()
afterEach ->
@sinon.restore()
jasmine.Spec::spy = (args...) ->
@sinon.spy args...
jasmine.Spec::stub = (args...) ->
@sinon.stub args...
# no mock, tests the real code
# ==================================================
# 1. Stub out ajax using Deferred #
#
# Use deferred object to trigger callback, so testing is synchronous. The
# resolve or reject are invoked, all requests are executed and returned.
#
# It also can be applied to any other library based on $.Deferred
jasmine.Spec::stubAjax = (object = $) ->
@stub object, 'ajax', (options) ->
# returns a new Deferred object so it supports all deferred methods, also invokes the callbacks in options
dfd = $.Deferred()
dfd.done(options.done) if options.done
dfd.done(options.success) if options.success
dfd.fail(options.fail) if options.fail
dfd.fail(options.error) if options.error
dfd.always(options.always) if options.always
dfd.always(options.complete) if options.complete
dfd.success = dfd.done
dfd.error = dfd.fail
dfd.complete = dfd.always
dfd
describe 'Stup Ajax', ->
beforeEach ->
@ajaxStub = @stubAjax()
@ajax = $.ajax
url: '/test'
success: @success = @spy()
error: @error = @spy()
complete: @complete = @spy()
it 'tests options', ->
# accept options can be checked in stub
expect(@ajaxStub.args[0][0].url).toEqual('/test')
it 'tests success', ->
# resolve with success arguments, should match the arguments for ajax success callback
@ajax.resolve(name: 'abc')
expect(@success.callCount).toBe(1)
expect(@success.args[0]).toEqual([name: 'abc'])
# or
spy = @spy()
@ajax.done(spy)
expect(spy.callCount).toBe(1)
expect(spy.args[0]).toEqual([name: 'abc'])
it 'tests failure', ->
# to test failure, should match the arguments for ajax error callback
@ajax.reject()
expect(@error.callCount).toBe(1)
expect(@error.args[0]).toEqual([])
# or
spy = @spy()
@ajax.fail(spy)
expect(spy.callCount).toBe(1)
expect(spy.args[0]).toEqual([])
it 'tests complete', ->
# both resolve and reject will trigger complete/always
@ajax.resolve()
# or ajax.reject()
expect(@complete.callCount).toBe(1)
expect(@complete.args[0]).toEqual([])
# ==================================================
# 2. Stub out jqxhr using sinon server (RECOMMENDED)
#
# Use respond to trigger callback, so testing is synchronous.
#
# This way you can get real AJAX response.
#
# It is also works for any ajax libraries.
# setup sinon server sandbox
afterEach ->
@server?.restore()
jasmine.Spec::fakeServer = ->
@server ?= sinon.fakeServer.create()
# ease respondWith
# The matching is processed in FIFO sequence, to add a catch all response, but that can be overrided
# later, set urlOrRegExp to null
jasmine.Spec::respondWith = (urlOrRegExp, options = {}) ->
type = options.type
code = options.code ? 200
headers = options.headers ? {}
data = options.data ? ''
contentType = options.contentType ? headers['Content-Type']
contentType = 'application/x-www-form-urlencoded' if contentType is 'form'
headers['Content-Type'] = contentType if contentType
unless type of data is 'string'
contentType ?= 'application/json'
headers['Content-Type'] = contentType
if /json$/.test(contentType)
data = JSON.stringify(data)
else if /x-www-form-urlencoded$/.test(contentType)
data = $.param(data)
if urlOrRegExp
if type
@fakeServer().respondWith(type, urlOrRegExp, [code, headers, data])
else
@fakeServer().respondWith(urlOrRegExp, [code, headers, data])
else
# if urlOrRegExp is falsy, use as default response, a.k.a, when no response matches, returns this
@fakeServer().respondWith([code, headers, data])
# All ajax requests are returned after call respond
jasmine.Spec::respond = -> @server?.respond()
describe 'Stup jqxhr', ->
beforeEach ->
@fakeServer()
@ajaxSpy = @spy($, 'ajax')
@ajax = $.ajax
url: '/test'
success: @success = @spy()
error: @error = @spy()
complete: @complete = @spy()
it 'tests options', ->
@respond()
# accept options can be checked in spy
expect(@ajaxSpy.args[0][0].url).toEqual('/test')
it 'tests success', ->
# simulate a server response
@respondWith '/test',
data:
name: 'abc'
# use respond to synchronize
@respond()
expect(@success.callCount).toBe(1)
expect(@success.args[0][0]).toEqual(name: 'abc')
# or
spy = @spy()
@ajax.done(spy)
expect(spy.callCount).toBe(1)
expect(spy.args[0][0]).toEqual(name: 'abc')
it 'tests failure', ->
# to test failure, should match the arguments for ajax error callback
@respondWith '/test',
code: 500
data:
error: 'abc'
@respond()
expect(@error.callCount).toBe(1)
expect(@error.args[0][0].status).toBe(500)
expect(@error.args[0][0].responseText).toEqual('{"error":"abc"}')
# or
spy = @spy()
@ajax.fail(spy)
expect(spy.callCount).toBe(1)
expect(spy.args[0][0].status).toBe(500)
it 'tests complete', ->
# both failure and success trigger complete/always
@respondWith '/test',
code: 500
data:
error: 'abc'
@respond()
expect(@complete.callCount).toBe(1)
# ==================================================
# 3. Really want to test with real server?
#
# Since ajax requests are asynchronuse, use Deferred pipe and runs/waitsFor to
# synchronize testing.
#
# This is also work for any asynchronize library that written based on Deferred.
#
# Setup a resolved deferred as pipe start point
beforeEach ->
@pipePromise = $.Deferred().resolve().promise()
# See jQuery Deferred.pipe
#
# Deferred status is filerted.
jasmine.Spec::pipe = (success, error) ->
# wrap callbacks with current context
context = @
if success
successWrapper = -> success.call(context, arguments...)
if error
errorWrapper = -> error.call(context, arguments...)
# setup args to ease test
chained = @pipePromise.pipe(successWrapper, errorWrapper)
chained.always (args...) -> chained.args = args
@pipePromise = chained
jasmine.Spec::waitsForPipe = (message = 'Waits for Spec pipe', timeout = 5000) ->
waitsFor ->
@pipePromise.state() in ['rejected', 'resolved']
, message, timeout
jasmine.Spec::expectPipeResolved = ->
runs ->
expect(@pipePromise.state()).toEqual('resolved')
jasmine.Spec::expectPipeResolvedWith = (args...) ->
runs ->
expect(@pipePromise.state()).toEqual('resolved')
expect(@pipePromise.args).toEqual(args)
jasmine.Spec::expectPipeRejected = ->
runs ->
expect(@pipePromise.state()).toEqual('rejected')
jasmine.Spec::expectPipeRejectedWith = (args...) ->
runs ->
expect(@pipePromise.state()).toEqual('rejected')
expect(@pipePromise.args).toEqual(args)
describe 'Synchronize real ajax', ->
it 'tests 404', ->
@pipe -> $.ajax '/not_found_page.html'
# must wait for all deferred in pipe finished
@waitsForPipe()
@expectPipeRejected()
# code that executed after pipe is finished must be contained in runs block
runs ->
expect(@pipePromise.args[0].status).toBe(404)
it 'tests success', ->
@pipe -> $.ajax './'
@waitsForPipe()
@expectPipeResolved()
# code that executed after pipe is finished must be contained in runs block
runs ->
expect(@pipePromise.args[2].status).toBe(200)
it 'multiple requests', ->
step1 = @pipe -> $.ajax './'
# execute when former request is success
@pipe -> $.ajax '/not_found_page.html'
@waitsForPipe()
@expectPipeRejected()
# code that executed after pipe is finished must be contained in runs block
runs ->
expect(step1.state()).toBe('resolved')
expect(step1.args[0]).toContain('<html')
expect(step1.args[2].status).toBe(200)
expect(@pipePromise.args[0].status).toBe(404)
it 'pipe error', ->
step1 = @pipe -> $.ajax '/not_found_page.html'
# to pipe when former request is failed, use the second argument
@pipe null, -> $.ajax './'
@waitsForPipe()
@expectPipeResolved()
# code that executed after pipe is finished must be contained in runs block
runs ->
expect(step1.state()).toBe('rejected')
expect(step1.args[0].status).toBe(404)
expect(@pipePromise.args[2].status).toBe(200)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment