-
-
Save svnlto/2925360 to your computer and use it in GitHub Desktop.
3 ways to test ajax requests or other asynchronouse lib based on jquery deferred
This file contains hidden or 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
# 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