1 | <a href="http://promises-aplus.github.com/promises-spec">
|
2 | <img src="http://promises-aplus.github.com/promises-spec/assets/logo-small.png"
|
3 | align="right" valign="top" alt="Promises/A+ logo" />
|
4 | </a>
|
5 |
|
6 | # Chai Assertions for Working with Promises
|
7 |
|
8 | **Chai as Promised** extends [Chai][chai] with a fluent language for asserting facts about [promises][presentation].
|
9 |
|
10 | Instead of manually wiring up your expectations to a promise's fulfilled and rejected handlers:
|
11 |
|
12 | ```javascript
|
13 | doSomethingAsync().then(
|
14 | function (result) {
|
15 | result.should.equal("foo");
|
16 | done();
|
17 | },
|
18 | function (err) {
|
19 | done(err);
|
20 | }
|
21 | );
|
22 | ```
|
23 |
|
24 | you can write code that expresses what you really mean:
|
25 |
|
26 | ```javascript
|
27 | doSomethingAsync().should.eventually.equal("foo").notify(done);
|
28 | ```
|
29 |
|
30 | or if you have a testing framework that follows the [UncommonJS specification][uncommonjs] for handling promises,
|
31 | simply
|
32 |
|
33 | ```javascript
|
34 | return doSomethingAsync().should.eventually.equal("foo");
|
35 | ```
|
36 |
|
37 | ## How to Use
|
38 |
|
39 | ### `should`/`expect` Interface
|
40 |
|
41 | The most powerful extension provided by Chai as Promised is the `eventually` property. With it, you can transform any
|
42 | existing Chai assertion into one that acts on a promise:
|
43 |
|
44 | ```javascript
|
45 | (2 + 2).should.equal(4);
|
46 |
|
47 | // becomes
|
48 | return promiseFor(2 + 2).should.eventually.equal(4);
|
49 |
|
50 |
|
51 | expect({ foo: "bar" }).to.have.property("foo");
|
52 |
|
53 | // becomes
|
54 | return expect(promiseFor({ foo: "bar" })).to.eventually.have.property("foo");
|
55 | ```
|
56 |
|
57 | There are also a few promise-specific extensions, grouped here as synonymic blocks (with the usual `expect`
|
58 | equivalents):
|
59 |
|
60 | ```javascript
|
61 | return promise.should.be.fulfilled;
|
62 |
|
63 | return promise.should.eventually.eql("foo");
|
64 | return promise.should.become("foo");
|
65 |
|
66 | return promise.should.be.rejected;
|
67 | return promise.should.be.broken;
|
68 |
|
69 | return promise.should.be.rejected.with(Error);
|
70 | return promise.should.be.broken.with(Error);
|
71 |
|
72 | // Note: other variants of Chai's existing `throw` assertion work too.
|
73 | ```
|
74 |
|
75 | ### `assert` Interface
|
76 |
|
77 | As with the `should`/`expect` interface, Chai as Promised provides an `eventually` extender to `chai.assert`, allowing
|
78 | any existing Chai assertion to be used on a promise:
|
79 |
|
80 | ```javascript
|
81 | assert.equal(2 + 2, 4, "This had better be true");
|
82 |
|
83 | // becomes
|
84 | return assert.eventually.equal(promiseFor(2 + 2), 4, "This had better be true, eventually");
|
85 | ```
|
86 |
|
87 | And there are, of course, promise-specific extensions:
|
88 |
|
89 | ```javascript
|
90 | return assert.isFulfilled(promise, "optional message");
|
91 |
|
92 | return assert.eventually.deepEqual(promise, "foo", "optional message");
|
93 | return assert.becomes(promise, "foo", "optional message");
|
94 |
|
95 | return assert.eventually.notDeepEqual(promise, "foo", "optional message");
|
96 | return assert.doesNotBecome(promise, "foo", "optional message");
|
97 |
|
98 | return assert.isRejected(promise, "optional message");
|
99 | return assert.isBroken(promise, "optional message");
|
100 |
|
101 | return assert.isRejected(promise, Error, "optional message");
|
102 | return assert.isBroken(promise, Error, "optional message");
|
103 |
|
104 | return assert.isRejected(promise, /error message matcher/, "optional message");
|
105 | return assert.isBroken(promise, /error message matcher/, "optional message");
|
106 | ```
|
107 |
|
108 | ### Progress Callbacks
|
109 |
|
110 | Chai as Promised does not have any intrinsic support for testing promise progress callbacks. The properties you would
|
111 | want to test are probably much better suited to a library like [Sinon.JS][sinon], perhaps in conjunction with
|
112 | [Sinon–Chai][sinon-chai]:
|
113 |
|
114 | ```javascript
|
115 | var progressSpy = sinon.spy();
|
116 |
|
117 | return promise.then(null, null, progressSpy).then(function () {
|
118 | progressSpy.should.have.been.calledWith("33%");
|
119 | progressSpy.should.have.been.calledWith("67%");
|
120 | progressSpy.should.have.been.calledThrice;
|
121 | });
|
122 | ```
|
123 |
|
124 | ### Working with Non-Promise–Friendly Test Runners
|
125 |
|
126 | As mentioned, many test runners (\*cough\* [Mocha][mocha-makes-me-sad] \*cough\* … but see [Mocha as Promised][]!)
|
127 | don't support the nice `return` style shown above. Instead, they take a callback indicating when the asynchronous test
|
128 | run is over. Chai as Promised adapts to this situation with the `notify` method, like so:
|
129 |
|
130 | ```javascript
|
131 | it("should be fulfilled", function (done) {
|
132 | promise.should.be.fulfilled.and.notify(done);
|
133 | });
|
134 |
|
135 | it("should be rejected", function (done) {
|
136 | otherPromise.should.be.rejected.and.notify(done);
|
137 | });
|
138 | ```
|
139 |
|
140 | In these examples, if the conditions are not met, the test runner will receive an error of the form `"expected promise
|
141 | to be fulfilled but it was rejected with [Error: error message]"`, or `"expected promise to be rejected but it was
|
142 | fulfilled."`
|
143 |
|
144 | There's another form of `notify` which is useful in certain situations, like doing assertions after a promise is
|
145 | complete. For example:
|
146 |
|
147 | ```javascript
|
148 | it("should change the state", function (done) {
|
149 | otherState.should.equal("before");
|
150 | promise.should.be.fulfilled.then(function () {
|
151 | otherState.should.equal("after");
|
152 | }).should.notify(done);
|
153 | });
|
154 | ```
|
155 |
|
156 | Notice how `.notify(done)` is hanging directly off of `.should`, instead of appearing after a promise assertion. This
|
157 | indicates to Chai as Promised that it should pass fulfillment or rejection directly through to the testing framework.
|
158 | Thus, the above code will fail with a Chai as Promised error (`"expected promise to be fulfilled…"`) if `promise` is
|
159 | rejected, but will fail with a simple Chai error (`expected "before" to equal "after"`) if `otherState` does not change.
|
160 |
|
161 | Another example of where this can be useful is when performing assertions on multiple promises:
|
162 |
|
163 | ```javascript
|
164 | it("should all be well", function (done) {
|
165 | Q.all([
|
166 | promiseA.should.become("happy"),
|
167 | promiseB.should.eventually.have.property("fun times"),
|
168 | promiseC.should.be.rejected.with(TypeError, "only joyful types are allowed")
|
169 | ]).should.notify(done);
|
170 | });
|
171 | ```
|
172 |
|
173 | This will pass any failures of the individual promise assertions up to the test framework, instead of wrapping them in
|
174 | an `"expected promise to be fulfilled…"` message as would happen if you did
|
175 | `Q.all([…]).should.be.fulfilled.and.notify(done)`.
|
176 |
|
177 | ### Compatibility
|
178 |
|
179 | Chai as Promised is compatible with all promises following the [Promises/A+ specification][spec]. Notably, jQuery's
|
180 | so-called “promises” are not up to spec, and Chai as Promised will not work with them. In particular, Chai as Promised
|
181 | makes extensive use of the standard [transformation behavior][] of `then`, which jQuery does not support.
|
182 |
|
183 | ## Installation and Setup
|
184 |
|
185 | ### Node
|
186 |
|
187 | Do an `npm install chai-as-promised` to get up and running. Then:
|
188 |
|
189 | ```javascript
|
190 | var chai = require("chai");
|
191 | var chaiAsPromised = require("chai-as-promised");
|
192 |
|
193 | chai.use(chaiAsPromised);
|
194 | ```
|
195 |
|
196 | You can of course put this code in a common test fixture file; for an example using [Mocha][mocha], see
|
197 | [the Chai as Promised tests themselves][fixturedemo].
|
198 |
|
199 | ### AMD
|
200 |
|
201 | Chai as Promised supports being used as an [AMD][amd] module, registering itself anonymously (just like Chai). So,
|
202 | assuming you have configured your loader to map the Chai and Chai as Promised files to the respective module IDs
|
203 | `"chai"` and `"chai-as-promised"`, you can use them as follows:
|
204 |
|
205 | ```javascript
|
206 | define(function (require, exports, module) {
|
207 | var chai = require("chai");
|
208 | var chaiAsPromised = require("chai-as-promised");
|
209 |
|
210 | chai.use(chaiAsPromised);
|
211 | });
|
212 | ```
|
213 |
|
214 | ### `<script>` tag
|
215 |
|
216 | If you include Chai as Promised directly with a `<script>` tag, after the one for Chai itself, then it will
|
217 | automatically plug in to Chai and be ready for use:
|
218 |
|
219 | ```html
|
220 | <script src="chai.js"></script>
|
221 | <script src="chai-as-promised.js"></script>
|
222 | ```
|
223 |
|
224 |
|
225 | [presentation]: http://www.slideshare.net/domenicdenicola/callbacks-promises-and-coroutines-oh-my-the-evolution-of-asynchronicity-in-javascript
|
226 | [chai]: http://chaijs.com/
|
227 | [mocha]: http://visionmedia.github.com/mocha/
|
228 | [mocha-makes-me-sad]: https://github.com/visionmedia/mocha/pull/329
|
229 | [Mocha as Promised]: https://github.com/domenic/mocha-as-promised
|
230 | [uncommonjs]: http://kriskowal.github.com/uncommonjs/tests/specification
|
231 | [spec]: http://promises-aplus.github.com/promises-spec/
|
232 | [transformation behavior]: https://gist.github.com/3889970#that-second-paragraph
|
233 | [fixturedemo]: https://github.com/domenic/chai-as-promised/tree/master/test/
|
234 | [amd]: https://github.com/amdjs/amdjs-api/wiki/AMD
|
235 | [sinon]: http://sinonjs.org/
|
236 | [sinon-chai]: https://github.com/domenic/sinon-chai
|