Dredd で OpenAPI (旧Swagger) をテストするときのテクニック

[Dredd](https://dredd.org/en/latest/) を用いると、実際にリクエストを発生させて OpenAPI の仕様通りにレスポンスが返されているかを確認することができます。ところが、 Dredd は元々 [API Blueprint](https://apiblueprint.org/) 向けに作られたという事情から、 OpenAPI の扱いがイマイチな面があるので、ワークアラウンドが必要になるケースがでてきます。本格的に使うと（自分で書くよりマシとはいえ）時々ちょっと苦労します。

## サポートされていない `format` で fail する問題

`format: uuid` など、他のツールではサポートされているデータ型がサポートされていない場合があります。このとき、理想的にはそこは無視して可能な限りのバリデーションだけを行ってほしいところですが、Dreddはバリデーションエラーを発生させてしまうようです。また、 `x-` のようなベンダープレフィックスを利用して、より厳密にチェックしたい場合もあると思います。

追記：[解決されました](https://github.com/apiaryio/dredd/issues/1218#issuecomment-586984343)

### 回避策

一旦 `pattern` で正規表現に置き換えてテストする方法が考えられます。

```js
function patchSchema(v, patch) {
  if (v.type === 'object') {
    const props = Object.values(v.properties);
    for (let i = 0; i < props.length; i += 1) {
      patchSchema(props[i], patch);
    }
  } else if (v.type === 'array') {
    patchSchema(v.items, patch);
  } else {
    patch(v, patch);
  }
}

let stash = null;

hooks.beforeEach((transaction, done) => {
  stash = {};
});

hooks.beforeEachValidation((transaction) => {
  const { bodySchema } = transaction.expected;
  stash.originalSchema = bodySchema;

  const bodySchemaObject = JSON.parse(bodySchema);
  patchSchema(bodySchemaObject, (v) => {
    if (v.type === 'string' && v.format === 'uuid') {
      const hex = '[0-9a-f]';
      v.pattern = `${hex}{8}-${hex}{4}-4${hex}{3}-[89ab]${hex}{3}-${hex}{12}`;
      delete v.format;
    }
  });
  transaction.expected.bodySchema = JSON.stringify(bodySchemaObject);
});

hooks.afterEach((transaction) => {
  transaction.expected.bodySchema = stash.originalSchema;
});
```

`patchSchema` で、 `object` と `array` をたどりながら、第二引数の `patch` で必要なワークアラウンドを実行しています。 `bodySchema` は `$ref` が解決されたスキーマが来るからたぶん大丈夫なはず･･･。今回の場合は UUIDv4 の正規表現に置き換えています。残念ながら正規表現で済まない場合は、 `patch` で `x-` から始まるマーカーを付けて `afterEach` で同様にたどりながら検証しましょう。

## `operationId` 使いたい問題

`summary` は後で変えたくなりそうだし、 Hook の `before` とかは `operationId` で指定したいよねという問題。

Dredd のテストケースは `filename > apiName > resourceGroupName > resourceName > actionName > exampleName` で特定されます。これは OpenAPI の場合は `filename > info.title > Operationのtagsの最初 > path > Operationのsummary > responsesのステータスコード > produces` で対応づけられているようです（省略の条件とかがありそうだけどよくわからない）。たぶん `summary` が必須で `operationId` が必ずあるとは限らないからなんだろうけど、それでもまともなコードを自動生成したい場合は皆入れてるはず（だよね？）。

### 回避策

`beforeEach` で `transaction` の値から `operationId` 特定して、定義した関数を呼ぶ。

```js
const fs = require('fs');
const yaml = require('js-yaml');

class OpenAPIDocument {
  constructor(filename) {
    const content = fs.readFileSync(filename, { encoding: 'utf-8' });
    this.schema = filename.endsWith('.json') ? JSON.parse(content) : yaml.safeLoad(content);
  }

  removeBasePath(path) {
    return path.substr((this.schema.basePath || '').length);
  }

  findOperation(method, path) {
    return this.schema.paths[path][method.toLowerCase()];
  }

  findOperationByTransaction(transaction) {
    const path = this.removeBasePath(transaction.origin.resourceName);
    return this.findOperation(transaction.request.method, path);
  }
}

let doc = null;
const testcases = new TestCases();

hooks.beforeEach((transaction, done) => {
  if (!doc) {
    doc = new OpenAPIDocument(transaction.origin.filename);
  }

  const operation = doc.findOperationByTransaction(transaction);
  const handler = testcases.beforeHandler(operation.operationId, transaction.expected.statusCode);
  if (handler) {
    transaction.skip = false;
    handler(transaction).then(done);
  } else {
    transaction.skip = true;
    done();
  }
});
```

としておいて、別な場所で `TestCases` を作る。

```js
class TestCases {
  constructor(baseURL) {
    this.testcases = {
      signIn: {
        async 200(transaction) {
          transaction.request.body = JSON.stringify({
            username: 'foo',
            password: 'bar',
          });
        },
        async 401(transaction) {
          transaction.request.body = JSON.stringify({
            username: 'foo',
            password: 'WRONG PASSWORD',
          });
        },
      },
    };
  }

  find(operationId, statusCode) {
    const testcases = this.testcases[operationId];
    return testcases ? testcases[statusCode] : null;
  }

  beforeHandler(operationId, statusCode) {
    const handler = this.find(operationId, statusCode);
    if (!handler) {
      return null;
    }
    if ('before' in handler) {
      return handler.before;
    }
    return handler;
  }

  afterHandler(operationId, statusCode) {
    const handler = this.find(operationId, statusCode);
    return handler ? handler.after : null;
  }
};
```

`this.testcases.[operationId]` には `async` 関数か `{before: async関数, after: async関数}` が入る。 `produces` が複数ある場合以外はカバーできる。テストケースが多くなってきた場合には、複数のファイルに分割して [スプレッド構文](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax) で統合すると良い。

ちなみに `testcases` に定義されていない場合はスキップするようにしている。なだらかに導入したいときに便利。

実は正常系のリクエストを生成するのが一番大変で、空のリクエストで 400 、不正なアクセストークン追加して 401 、権限の無いアクセストークンで 403 、正常なアクセストークンで存在しない ID（←Dreddが生成してる！）にすると 404 、存在する ID で 200 、それを作成しようとして 409 と、ステータスコードの番号順に考えていけばほとんどの場合はカバーできる。