Inspired by "You're Missing the Point of Promises," I wrote this test suite for the CommonJS Promises/A spec and some of its common extensions. If you're not passing this, something's wrong.
This test suite is no longer maintained. It has been superceded by the Promises/A+ test suite, which tests against the much better Promises/A+ spec. That spec takes more care with various edge cases, and subsumes most of the common extensions listed under "Other Included Tests" below. And the test suite is much more comprehensive, as well.
Nevertheless, the original README is left below for historical interest. But please, switch!
The tests run in a Node.js environment; make sure you have that installed.
In order to test your promise library, you must expose a very minimal adapter interface. These are written as Node.js
modules with a few well-known exports. Check out some examples in lib/adapters
, and read the README.md
file there
for guidance and a more in-depth explanation.
This package comes with a command-line interface that can be used either by installing it globally with
npm install promise-tests -g
or by including it in your package.json
's devDependencies
and using npm's scripts
feature. In the latter case, your setup might look something like
{
"devDependencies": {
"promise-tests": "*"
},
"scripts": {
"test": "run-my-own-tests && promise-tests all test/my-promise-tests-adapter"
}
}
The CLI takes two arguments: the test suite you want to run (either promises-a
or all
), and the filename of your
adapter file, relative to the current working directory. If either of these is missing, it will prompt you for them
interactively.
The main export of this package is a function that allows you to run the tests against an adapter:
var promiseTests = require("promise-tests");
promiseTests(adapter, ["promises-a"], function () {
// All done, output in the CLI.
});
The second parameter is an array containing which tests you want to run (see below).
Promises/A is a rather bare spec. Most promise implementations have converged on certain semantics which make working
with promises much more pleasant. Those tests are included in other files in the lib
directory, and can be run with
through the CLI with the all
option, or individually with the programmatic option.
There is, unfortunately, a very common and important behavior of thenables that is not in the Promises/A spec: what happens when one of your handlers returns a promise? For concreteness, let's use this example:
var a = b.then(function () {
return c; // `c` is a promise
});
Most implementations have converged on the answer that a
should be resolved in the same way as c
, i.e.
a
should be fulfilled if and only if c
is fulfilled, and with c
's fulfillment valuea
should be rejected if and only if c
is rejected, and with c
's rejection reasonUnfortunately the Promises/A spec alone seems to imply that a
should always be fulfilled, with the promise c
as its
fulfillment value!
Tests for this spec extension are included as returning-a-promise
.
As described in the "Requirements" section of the CommonJS wiki on Promises, number 3.2, you should be able to
distribute the resolver to multiple mutually-suspicious consumers, and have them "race" to resolve the promise. This is
somewhat analogous to the synchronous case where there can be a "race" between multiple return
and throw
statements
within the same function. It's useful for implementing cases like a race between a timeout rejection and a normal
resolution, as in Q's Q.timeout(promise, ms)
. And it has some security implications in the
object-capability sense.
In particular, this means that resolvers (i.e. someone with only the ability to fulfill or reject a promise) should not be able to observe the state of the promise so far. For example, attempting to resolve multiple times should not throw an error, since that would be a way for someone with only resolver capabilities to determine a promise's state. However, the Promises/A spec itself failed to capture this requirement, even though the CommonJS group considered it important, so implementations are still Promises/A conforming if they throw errors.
Tests for this spec extension are included as resolution-races
.
It's generally more predictable if you're guaranteed that your handlers are always called in a future turn of the event loop. This allows you to know the execution order of code like the following with confidence:
console.log("1");
promise.then(function () {
console.log("3");
});
console.log("2");
If a promise library does not guarantee asynchronicity, then in some cases the sequence will be 1, 2, 3, while in others it will be 1, 3, 2. This makes code hard to follow as your assumptions about what is true inside the handler do not always hold.
For example, consider a promise-returning library for storing data that does not guarantee asynchronicity. You may be
using the localStorage
backing store, which is always synchronous, leading you to expect the 1, 3, 2 sequence and
write code that assumes changes were committed by the time 2 gets logged to the console. But later, you take advantage
of this hypothetical library's great flexibility to switch to an IndexedDB
backing store, which happens to be
always-asynchronous. Now your code takes the 1, 2, 3 path, breaking your earlier assumption and introducing tons of
subtle bugs.
To avoid this problem, leading promise libraries are sure to always call handlers in the next turn of the event loop,
using mechanisms like process.nextTick
in Node or setTimeout(..., 0)
in browsers. That way, promise producers can
resolve their promises either synchronously or asynchronously, without worrying that promise consumers will face
different behavior.
Tests for this spec extension are included as always-async
I'd like this to run more easily in the browser, for libraries like Ember or jQuery (even though in the latter case I've hacked together a jsdom-based solution).
There are other spec extensions that would be useful to test, e.g. the behavior of deferreds, which are more or less the canonical promise-creation technique. There are a few subtleties there regarding resolving a deferred with a pending promise that not everyone gets right.