2019-10-21

thenから始めないといけないキモいオブジェクトで、stub/mockとか頑張る

Swagger Clientの設計がキモい

swagger-client 3.9.5 で確認。

swagger-api/swagger-js: Javascript library to connect to swagger-enabled APIs via browser or nodejs

いろいろあるけどいちばんキモいのはコレ。

const Swagger = require('swagger-client')

const s = new Swagger()
s
.then(client => {
  ..
})
.catch(e => {
  ..
})

これ、インスタンスが Promise そのものなので、まず then を書かないと実際の Swagger Client を取得できずに何も実行することができない。何らかのメソッドが Promise を返すのは分かるけどインスタンスそのものって、さすがにどうなの。

これって単に「自由な callback メソッドだと引数の順番のルールが統一されていないので、とりあえず Promise の仕様に合わせたよ」以上の意味がないように見えるんだけど、どうなんだろうなぁ。

PromiseになってしまったオブジェクトのメソッドをwrapするPromiseを作ってawaitできるように

上のような Promise は何も動作し始めていないので async/await できない。無限に wait して Timeout する。これは困る。then / catch の世界は結局 callback であり1、scope が切れて扱いにくい。

そこで何らかのメソッドを呼ぶための wrapper を書いて、そいつが Promise を返すようにすると async/await で書けるようになる。上の例で言うと以下のような感じ。

async foo (retry) {
  return new Promise((resolve, reject) => {
    s
    .then(async (client) => {
      const r = await client.execute()
      resolve(r)
    })
    .catch(async (err) => {
      if (retry > 0) {
        return await foo(--retry).then(resolve).catch(reject)
      } else {
        return reject(err)
      }
    })
  })
}

ここではあえて古式ゆかしい Promise の書き方をしている。.then().catch() で数珠つなぎにしてる場合はそのまま値を return することはできないので async/await のように楽することができない。しっかり return new Promise() で resolve, reject してあげよう。こうしておかないと var を使って破壊的に値を代入したうえでその値をチェックして return するか throw するか、みたいな余計なロジックが必要になってしまう。

上のような Promise を作ると async/await の書き方とマッチする。

プロパティに収まった何もしないPromiseに対してstub, spyを作る

上の書き方で扱いにくい Promise を async/await で書けるようになったわけだが、まだ問題がある。テストコードである。

例えば c というオブジェクトの中に s という property で Swagger インスタンスを持っているとする。

class C {
  constructor () {
    this.s = new Swagger()
  }
}
const c = new C()

sinon で stub out する場合、then の方はこんな感じ。

sinon.stub(c, 's').get(() => {
  return Promise.resolve({
    async execute () {
      return {
        ..
      }
    }
  })
})

Promise.resolve() で client に相当するオブジェクトを作って返してやる。例えば client.execute() の実行が必要ならそれに対して適当な値が返ってくるように仕立ててやるとその周辺のコードのテストができる。async/await 時代はこういうコードがだいぶ書きやすくなってて助かる。

catch の方はこんな感じになる。

sinon.stub(c, 's').get(() => {
  return Promise.reject(..)
})

考え方は同様。Promise.reject() で Error オブジェクトを作って返してやる。

ポイントは sinon.stub().get() で、これは getter メソッドの stub out に使える記法。

少しまとめると、

  • そもそも stub out はメソッドに対して行うものだが、どうしても property に対して行いたいということであれば、property を getter メソッドと看做して stub().get() を使うことで対応できる
  • then() と catch() それぞれのテストは stub().get() で Promise.resolve() か Promise.reject() を return してあげることで kick できる

sinon の部分の続きは Sinon.JSにもう少し詳しくなったのでちょっとまとめ - あーありがち(2019-10-22) へ。

他の test double も同様の考え方を適用できるんだと思う。試してない。悪しからず。

execute を実環境に対して実行するテストはいろいろ重いので、平時は execute() に対して何らかの固定値を返すものを用意しておいて、その後の動作をテストする形になると思う。

  1. 深くなりにくいだけ 

About

例によって個人のなんちゃらです