Created
June 2, 2012 13:32
-
-
Save jonnyreeves/2858452 to your computer and use it in GitHub Desktop.
Unit Testing Promises with Sinon.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
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> | |
<html> | |
<head> | |
<link rel="stylesheet" href="http://code.jquery.com/qunit/git/qunit.css" type="text/css" media="screen" /> | |
<!-- when.js Promises implementation --> | |
<script src="https://raw.github.com/cujojs/when/master/when.js"></script> | |
<!-- Unit testing and mocking framework --> | |
<script type="text/javascript" src="http://code.jquery.com/qunit/git/qunit.js"></script> | |
<script type="text/javascript" src="http://sinonjs.org/releases/sinon-1.3.4.js"></script> | |
<script type="text/javascript" src="http://sinonjs.org/releases/sinon-qunit-1.0.0.js"></script> | |
<!-- test suite --> | |
<script src="whenjs-examples.js"></script> | |
</head> | |
<body> | |
<h1 id="qunit-header">Unit Testing when.js Promises with QUnit and SinonJS</h1> | |
<h2 id="qunit-banner"></h2> | |
<div id="qunit-testrunner-toolbar"></div> | |
<h2 id="qunit-userAgent"></h2> | |
<ol id="qunit-tests"></ol> | |
<div id="qunit-fixture"></div> | |
</body> | |
</html> |
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
function createClient() { | |
return { | |
// Classic async method which returns an un-resolved promise. | |
fetchAsync: function () { | |
var dfd = when.defer(); | |
// This contrived example isn't the important bit, replace this | |
// with a jQuery.get call, or any other async action which yeilds | |
// a promise to the caller. | |
setTimeout(function () { | |
dfd.resolve("Response"); | |
}, 1500); | |
return dfd.promise; | |
}, | |
// Slightly different take, this time a Promise resolver is supplied | |
// to the method. | |
asyncInit: function (completionPromise) { | |
// Again, the implementation isn't important - but after an async | |
// delay this method will either resolve, or reject the supplied | |
// connection promise. | |
setTimeout(function () { | |
completionPromise.resolve("I'm done!"); | |
}, 1500); | |
// Note the lack of return statement. | |
} | |
} | |
}; | |
// QUnit's way of defining a TestSuite. The 'setup' method below will be | |
// invoked before each 'test' case. | |
module("when.js examples", { | |
// Setup is invoked before each test run. | |
setup: function () { | |
// Create a fresh test client instance to work with. | |
this.client = createClient(); | |
} | |
}); | |
test("Stub a promise to always return a resolved Promise", function () { | |
// Stub the promise so it returns a fully resolved promise. | |
var expectedResponse = "what we expect to get back... eventaully"; | |
sinon.stub(this.client, 'fetchAsync').returns(when(expectedResponse)); | |
// Call the method and run the assertion | |
this.client.fetchAsync().then(function (actualResponse) { | |
strictEqual(actualResponse, expectedResponse, "fetchAsync() returned a" | |
+ " resolved Promise with the expected response"); | |
}); | |
}); | |
test("Stub a promise to always return a rejected Promise", function () { | |
// This time we make 'fetchAsync' return a rejected promise | |
var expectedError = new Error("KaBOOM!"); | |
sinon.stub(this.client, 'fetchAsync').returns(when.reject(expectedError)); | |
// Call the method and expect it to fail. | |
this.client.fetchAsync().otherwise(function (actualError) { | |
strictEqual(actualError, expectedError, "fetchAsync() returned a" | |
+ " rejected Promise with the expected error"); | |
}); | |
}); | |
test("Stub a promise so the testcase has control over it", function () { | |
// Sometimes you want the testcase to hold onto the Promise so you can | |
// assert the state of other parts of the system while the Promise is | |
// still un-resolved. Start by creating a new Promise. | |
var fetchPromise = when.defer(); | |
// Now stub fetchAsync so it returns this Promise. | |
sinon.stub(this.client, 'fetchAsync').returns(fetchPromise); | |
// Some example state, just to show we have control over the Promise. | |
var didWeResolveThePromiseYet = false; | |
// Call the method, but it won't have resolved yet... | |
this.client.fetchAsync().then(function () { | |
equal(didWeResolveThePromiseYet, true, "Test case dictated when the" | |
+ " Promise resolved"); | |
}); | |
// Ok, now let's flip that flag. | |
didWeResolveThePromiseYet = true; | |
// ... and resolve the promise. | |
fetchPromise.resolve("Go for it!"); | |
}); | |
test("Stub a method so it resolves the supplied promise", function () { | |
// Stub the asyncInit method so it calls the 'resolve' method of the | |
// supplied promise. | |
var expectedResult = "we get this back once asyncInit's done it's thing"; | |
sinon.stub(this.client, 'asyncInit').yieldsTo("resolve", expectedResult); | |
// This is the promise we supply to asyncInit(). | |
var initDeffered = when.defer(); | |
// Attach our assertion to the completion handler. | |
initDeffered.promise.then(function (actualResult) { | |
strictEqual(actualResult, expectedResult, "asyncInit() resolves the" | |
+ " supplied Promise with the expected result"); | |
}); | |
// Invoke the client supplying the resolver | |
this.client.asyncInit(initDeffered.resolver); | |
}); | |
test("Stub a method is it rejects the supplied promise", function () { | |
// This time asyncInit will always reject the supplied Promise. | |
var expectedError = new Error("Ain't gonna happen"); | |
sinon.stub(this.client, 'asyncInit').yieldsTo("reject", expectedError); | |
// This is the deffered we supply to asyncInit(). | |
var initDeffered = when.defer(); | |
// Attach our assertion to the error handler. | |
initDeffered.promise.otherwise(function (actualError) { | |
strictEqual(actualError, expectedError, "asyncInit() rejects the" | |
+ " supplied Promise with the expected error"); | |
}); | |
// Invoke the client supplying the resolver. | |
this.client.asyncInit(initDeffered.resolver); | |
}); |
@jonnyreeves This gist really is helpful for me. I was struggled to unit testing the promises method until I found this gist. Thank you!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This is good stuff! Unit testing with promises is def territory that needs more work and examples like this. Using sinon's
yieldTo
to trigger resolution/rejection is especially clever.Another approach that I've seen used is to actually mock or fake the promises themselves. For example, in some cases, it may make sense to stub a function and have it do something like this:
In some cases, that may be enough. If the code consuming the promise uses
when()
, that'll be even safer, sincewhen()
will defend it against any weird side effects of using a fake promise.I'd be interested to hear what you think about using fake promises like that.
One potential gotcha for folks to remember about using real promises is that some promise implementations, such as Q, force callbacks to be invoked in a future turn (i.e. asynchronously). There are valid reasons for doing that, but most of the popular implementations, like when.js, dojo Deferred, and jQuery Deferred, don't do that. Anyway, tests that might be expecting synchronous promise behavior might break if an async-forcing promise somehow made its way into the testing promise chain.
Anyway, great work, dude.
P.S. you can use
when/delay
to implementfetchAsync
andasyncInit
: