Docker を使って Dredd で openapi (Swagger) のテストを簡単にできるようにしたよ

## OpenaAPI (Swagger) の管理が本当に煩わしい


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632997/447e78af-dbb2-63a8-aad9-de867b57472b.png" width="300px">

OSA の管理は基本的にはサーバ側の責任なのかなと思います。
私はサーバサイドエンジニアですが、この管理が本当に大変だと身をもって感じています。
RESTでAPIを作るならSwaggerの仕様書が間違っているとFE/BEどちらも消耗してしまうこと山の如しです。
FEのエンジニアから「プロパティ名が一文字間違っていてハマりましたよー笑」なんて言われてしまうと、平謝りすることしかできないので、どうしてもこの辺りをちゃんとさせねばなりません。
(その説は本当にすみません)
しかし、工数が限られている中で、目視で何度も確認はあまりのも時間を消費しすぎてしまう。

そこで、自動テストツールである**Dredd**の出番です。

https://dredd.org/en/latest/


## Dredd と 分割されたOAS

例えば以下のように、外部ファイルを参照させて分割したOASの場合、 Dreddはテストをしてくれません。

```
      responses:
        "204":
          $ref: "responses/common/204.yml"
        "401":
          $ref: "responses/common/401.yml"
```

こうしたOASの場合、事前にファイルを統合する必要があります。

## Swagger Merger 

というわけで、ファイルの統合には **Swagger Merger** という素晴らしいライブラリがあります。
これを使ってファイルを統合し、その後に Dredd でテストで回しましょう。

https://github.com/WindomZ/swagger-merger

## Makefile でやっちゃおう

操作もそんなに複雑ではないし、Makefile大好きエンジニアなので、最初はMakefileを使って運用しました。

`make dredd-init` でライブラリをインストールしてもらって、
`make dredd` でテストを実行します。

openapi/default.yml がOASである場合、以下のように設定すればOK。

``` 
dredd-init: 
	@npm install swagger-merger -g
	@npm install -g dredd

dredd: 
	@swagger-merger -i openapi/default.yml -o openapi/merged.yml
	@dredd openapi/merged.yml http://localhost:8000/api/ -d  --hookfiles="openapi/hooks/*.js" 
```

## Dredd テストの制御

デフォルトでは、OASに書かれたすべてのレスポンスを検証します。
例えばすべてのエンドポイントに401が設定されていると、それぞれ401のレスポンスを検証してしまいます。
**hookファイル**を利用することで、テストをスキップさせるなど、テストの挙動を制御することができます。
上記のコマンドのうち `--hookfiles="openapi/hooks/*.js"` がどのhookファイルを利用するかを指定しています。
またhookファイルはjs以外の言語で書くこともできます。

認証が必要なエンドポイントについては、hookファイルで、特定のテストの前に Authorizationヘッダーにトークンをつけるなどの処理を挟まないと、正常系のテストができなかったりします。


## Dockerを使おう

各自 npm を使ってインストールしてもらっていましたが、 nodebrew のバージョンの切り替えを忘れてて動かないと連絡があったり、環境の問題で変な不具合が出たりと割り込みが入るので、やっぱりDocker化することにしました。

最初はDocker Compose で作っていましたが、常時起動するものでもないし、なんとなく使い勝手が悪かったので、シェルスクリプトで記述することにしました。

また、hookファイルは大体必須なので、プロジェクトごとにいちいち配置するのも面倒だと考えたので、OASがあるディレクトリに引っ張ってきてそのまま使えたらいいなと考えました。

というわけで、作ったものを置いておきます↓

https://github.com/itsuki-n22/dredd_cmd

使い方は、 OASがあるディレクトリに `git clone` して、 params.cfg でAPIのエンドポイントとOASのファイル名を指定するだけです。

## ↑スクリプトの解説

``` dredd.sh
docker run -it --rm -v $SCRIPT_DIR/..:/openapi \
--add-host=host.docker.internal:host-gateway \
apiaryio/dredd sh -c \
"dredd openapi/$DREDD_DIR/tmp/merged.yml $API_URL \
-d --hookfiles=$HOOKFILES --no-color | \
tee openapi/$DREDD_DIR/tmp/dredd_result.txt | \
grep -e 'pass\:' -e 'fail\:' -e 'complete\:'"
```

コマンドの本体部分は上記のようになっています。

Docker　を使っているのでコンテナからホストコンピュータのエンドポイントにアクセスするために`--add-host=host.docker.internal:host-gateway` を使っています。
これは、**Docker v 20.10.0 以降でないと利用できない**ので、それより古いバージョンを使っている人は、別途設定が必要になります。

```
tee openapi/$DREDD_DIR/tmp/dredd_result.txt | \
grep -e 'pass\:' -e 'fail\:' -e 'complete\:'"
```

この部分ですが、 Dreddのテスト結果は**とんでもなく長い**ので、ダイジェストだけをSTDOUTに出力するようにして、詳細はファイルに出力するようにしています。

本当にとんでもなく長いのです。


## Dreddハマりどころ

### Swagger-UI とは無関係

私が勘違いしていたのですが、Dreddのコマンド中のエンドポイントの指定先をSwagger-UIなどにしてしまうことです。

DreddはSwagger-UIを自動で動かすツールではありません。

DreddはOASを見て、検証先のAPIを自動で叩くツールなのです。

例えばRailsのAPIモードが localhost:3000 で動いているなら、

`dredd openapi/default.yml http://localhost:3000/api/`

のようにコマンドを打つ必要があります。

### OASの文法

openapiが推奨する書き方と違っていても、 Swagger-UI はよしなに解釈してUIを構築してくれます。

しかし、Dredd先生は大きな間違いがある場合にはテストを走らせてくれません。

私がひっかかった指摘が以下です

- description の項目がない
- object が正しく設定されていない
- responseでheaderの内容とcontentの内容が違い
- exampleの値が指定されていない
    - そもそも exampleの値を見てリクエストをテストするので、これが指定されていないとテストができない

### エンドポイント...

BEのリポジトリが２個存在するアプリで、Dreddを使っていたのですが、
Dreddのテスト中だけ、一方はJWT認証が通るのに、もう一方は認証が通らない！なんてことがありました。
SwaggerUIからは正常に動くのになぜ...
JWTの中身も別に間違ってないのに、なぜなんだと悩んでいましたが、DreddのAPIの**エンドポイントの向き先が間違っていて**、
もう一方のAPIに向いていて、JWTシークレットが違うから認証が通らないなんてことがありました。

ちなみに、エンドポイントが違うと気づくのに**N時間**かかり、かなり時間を浪費...
こういうこともあるので、エンドポイントは真っ先の確認は大事ですね！涙

Dreddの詳細な結果では、実際のレスポンスと、予想されたレスポンスが記載されるのですが、
実際のレスポンスが何もない場合、大体エンドポイントが違うだけです笑

こういうミスが無くなるように Makefile　なり スクリプトなりを使って管理した方がいいなと思いました。


## でも結局...

特に認証が入る場合、hookファイルのカスタマイズが必須で、これをプロジェクトに合ったものにしないと使い物になりません。
ただ、上記レポジトリのhook.jsの雛形と、以下のドキュメントを参考にすれば大体なんとかなると思います。

https://dredd.org/en/latest/data-structures.html

### エンドポイントの並び順

OASのエンドポイントを上から順番で叩くので、hookファイルの作成もそこを気にする必要がああります。

1. loginのエンドポイントを叩き、返ってきたトークンを Authroizationヘッダに載せる
2. 続いてrefresh のエンドポイントを叩く
3. 以下認証が必要なエンドポイントを順に叩く

こういう場合、refreshを叩くことで前のトークンが期限切れになる処理がある場合、当然1.で手に入れたトークンは使えなくなります。
しかし、これに気づかず、トークンの内容は正しいのになぜか 3.の正常系が動かずハマりました。