DeferredとPromise
PromiseをDeferredっぽく使いたかったけど、微妙に使えなかったのでうまくいく方法を探した( ⁰⊖⁰)
DeferredとPromiseの違い
JavaScriptのPromiseとjQueryのDeferredの間には微妙な差があります。
setTimeOutしてconsoleに表示を出すようなシンプルなサンプルで比べてみます。
上の例をみると、(コメントで書いたように)d.resolve()
とするか、new Promise(function (resolve, reject)
のように引数で受け取るかの違いがあることが分かります。
Deferredオブジェクトは本来then
やcatch
ができればよいはずなのに、どこでもresolve
, reject
出来てしまうのは、Promiseとして問題のある使い方を可能にしてしまいます。
問題のある使い方というのは、例えば、下のように非同期の処理を書いてresolve()
を呼ぶべき場所で、done
やfail
を指定したDeferredオブジェクトを返してしまい、外部からresolve
を呼び出すことで処理を終了させるような使い方です。
JavaScriptのPromiseでは、resolve()が外部からプロパティとして公開されていないので、このような使い方は出来なくなっています。
めでたし?
一方でTesting jQuery ajax with mocha and sinonでは、Deferredの微妙な挙動を活かした$.ajaxを内部で呼び出すメソッドのテスト方法が紹介されています。
詳細は省きますが、 before_and_after_each.jsとtests.jsを抜粋して紹介します。
サンプルコードでは、$.ajax
を次のようなdeferredオブジェクトを返すようなメソッドにstubしています。
返されるdeferredオブジェクトは、$.ajax
に渡されたoptions
から成功時の処理、失敗時の処理を得た後、deferredのdone
とfail
にそれぞれの処理を指定したものになっています。つまりresolve()
の時にはoptions.success
が、reject()
の時にはoptions.error
が呼ばれます。
sinon.stub($, 'ajax', function (options) { // Creating a deffered object var dfd = $.Deferred(); // assigns success callback to done. if(options.success) dfd.done(options.success({status_code: 200, data: {url: "bit.ly/aaaa"}})); // assigns error callback to fail. if(options.error) dfd.fail(options.error); dfd.success = dfd.done; dfd.error = dfd.fail; // returning the deferred object so that we can chain it. return dfd; });
こうすることによってテストを書く際に、テストしたいメソッド.resolve()
やテストしたいメソッド.reject()
と書くことで成功時、失敗時の処理が正しく行われているかどうかを簡単にチェックすることが出来るようになります。
it("yeild success", function (done) { url.shorten("http://google.com", this.callback).resolve(); // sinon will check whether the success method is called Once assert(this.callback.withArgs(0,"bit.ly/aaaa").calledOnce); done(); });
Promiseでも同じことをやりたい場合
先に紹介したように、Promiseではこのようなことはできません。しかし、jQueryを排除したいと考えている場合、Promiseを使って同じようなことをやりたくなるケースが出てきます(した)。
そこで下記のような、外部からresolve()
を呼べるような仕組みを作りました。
var deferred = function ...
の部分は再利用可能で、thenのように成功時と失敗時のコールバックを指定してやれば、上記のDeferredの例のようにresolve()
, reject()
を外から呼び出すことができます。
これでDeferredと同じような方法でテストを書くことが可能になりました。
it "call the ajax once", () -> ToDo.loadAll(sinon.spy()).resolve() assert(request.get.calledOnce) it "yield success", () -> callback = sinon.spy() {promise, resolve} = ToDo.loadAll(callback) promise.then(()-> assert(callback.withArgs(200, [todo]).calledOnce) done() ).catch((e) -> done(e)) resolve() it "yield error", () -> callback = sinon.spy() {promise, reject} = ToDo.loadAll(callback) promise.then(() -> assert(callback.withArgs(400).calledOnce) done() ).catch((e) -> done(e)) reject()
- コード -> matsu-chara/promise-helper-for-deferred-style-resolve · GitHub
- npm -> promise-helper-for-deferred-style-resolve
めでたしめでたし( ⁰⊖⁰)