Promiseを外部からresolveする方法

DeferredとPromise

PromiseをDeferredっぽく使いたかったけど、微妙に使えなかったのでうまくいく方法を探した( ⁰⊖⁰)

DeferredとPromiseの違い

JavaScriptPromisejQueryDeferredの間には微妙な差があります。

setTimeOutしてconsoleに表示を出すようなシンプルなサンプルで比べてみます。

上の例をみると、(コメントで書いたように)d.resolve()とするか、new Promise(function (resolve, reject) のように引数で受け取るかの違いがあることが分かります。

Deferredオブジェクトは本来thencatchができればよいはずなのに、どこでもresolve, reject出来てしまうのは、Promiseとして問題のある使い方を可能にしてしまいます。

問題のある使い方というのは、例えば、下のように非同期の処理を書いてresolve()を呼ぶべき場所で、donefailを指定したDeferredオブジェクトを返してしまい、外部からresolveを呼び出すことで処理を終了させるような使い方です。

JavaScriptのPromiseでは、resolve()が外部からプロパティとして公開されていないので、このような使い方は出来なくなっています。

めでたし?

一方でTesting jQuery ajax with mocha and sinonでは、Deferredの微妙な挙動を活かした$.ajaxを内部で呼び出すメソッドのテスト方法が紹介されています。

詳細は省きますが、 before_and_after_each.jstests.jsを抜粋して紹介します。

サンプルコードでは、$.ajaxを次のようなdeferredオブジェクトを返すようなメソッドにstubしています。

返されるdeferredオブジェクトは、$.ajaxに渡されたoptionsから成功時の処理、失敗時の処理を得た後、deferredのdonefailにそれぞれの処理を指定したものになっています。つまり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()

めでたしめでたし( ⁰⊖⁰)