HTTP(S)トラフィック検証用のAPIサーバを即座に提供するOSS - Hoverflyの紹介

## この記事を読んでできるようになること

* Hoverflyとは何か，どんな機能があるのかを知ることができる．
* HTTP(S)トラフィックの検証にHoverflyの利用を検討できる．

Hoverflyの[公式ドキュメント](https://docs.hoverfly.io/en/latest/index.html)を一通り読んで，ユースケースとしてまとめてみました．
「こんな使い方もあるよ！」「ここ違うんじゃない？」などあれば気軽にコメントください！

## Hoverflyとは

![logo-large.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/fdedc17b-2cab-465d-ed61-fe17baa38ee1.png)

https://docs.hoverfly.io/en/latest/#

**HTTP(S)トラフィックのキャプチャと高自由度の制御を提供するOSSのAPIシミュレーションツール**です．
Hoverflyは，キャプチャしたトラフィックを`Simulations`という名前のschema(論理構造)に基づいて処理し，もっともらしいresponseを返答します．
`Simulations`は，JSONファイルでexport/importによる管理が可能です．

具体的にいくつかの機能や特徴をリストアップしてみます．

* 簡単にシミュレーションサーバ（ウェブサーバ，プロキシサーバ）を作成できる．
* ステートフルな挙動も再現できる．
* Hoverflyを介するトラフィック/介さないトラフィックを自由に制御できる．
* request, responseの編集を自由に行えるので，アプリおよびアプリが依存する外部のサービスいずれも未完成の場合も利用できる．
* 特にresponseのbodyは，requestの値や乱数を用いて動的にも編集できる．
* 本番を模してdelayも簡単につけられる．
* ...

機能が豊富，かつユーザが制御できる範囲が非常に広い便利なツールであり，導入もすごく簡単なのですが，ググっても和訳がほとんど無いのでとっつきにくさがあるのかもと感じました．
~~ロゴがニガテという方もいるかもしれませんが...~~

## Hoverflyのユースケース

APIシミュレーションツールは，[トップダウンテスト](https://e-words.jp/w/%E3%83%88%E3%83%83%E3%83%97%E3%83%80%E3%82%A6%E3%83%B3%E3%83%86%E3%82%B9%E3%83%88.html)において[スタブ](https://e-words.jp/w/%E3%82%B9%E3%82%BF%E3%83%96.html)（モック）を提供するなどの用途に使用されることがしばしばです．
スタブとなるのはテスト対象アプリから切り出されたサービスなので，ここでは"外部サービス"と呼ぶことにします．
"外部サービス"のモックの作成にあたっては，プロキシサーバの応答をどのように含めるべきか悩ましいということもあると思います．
Hoverflyは，プロキシサーバの応答も自由に含めながら"外部サービス"のモックを作成して，**テスト用APIサーバ**を提供します．

次のHTTP(S)通信を行うテスト対象アプリに対して，モックの作成を考えてみたいと思います．

![スライド8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/1e5a86ae-7a48-f06b-29a2-0d92f4c22d57.png)

### ユースケース-1: テスト対象アプリが依存する"外部サービス"をモック化する．

![スライド9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/37990583-a338-16f6-db8b-9e93ad7c6ebc.png)

本番環境から切り離されたテスト環境に，（プロキシサーバの有無に関わらず）"外部サービス"の応答をモックとして持たせることができます．
既存のアプリを改修して速度を上げたり表示を刷新したりしたいという場合などが該当します．

### ユースケース-2: テスト対象アプリが依存する"外部サービス"をモック化する．改修中の"外部サービス"のrequest-response関係をモックに登録する．

![スライド10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/5a2cd54e-42da-ac5e-14c1-724f7280fe43.png)

ユースケース-1に加えて，任意のrequest-responseをモックに登録することもできます．
"外部サービス"も同時に改修する場合が該当します．

### ユースケース-3: 改修で追加されるrequest-response関係のみ"外部サービス"モック化する．既存のrequest-response関係は既存の"外部サービス"にて処理する．

![スライド11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/85175f98-33e0-0705-6960-33d19901c7e0.png)

テスト環境を本番環境と分離できない（分離しない）場合が該当します．
テストと並行して個人開発をする場合に利用したいことがあるかもしれません．
Hoverflyが責任を持つ範囲を最小限にすることで，`Simulations`の作成も簡易化できます．

### ユースケース-4: プロキシサーバをモック化する．

![スライド12.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/ee1a7f9e-f7d2-9b9f-ef57-4dab544d568f.png)

ユースケース-1~3では，プロキシサーバを含む/含まないに関係なく"外部サービス"として応答を持つモックを作成していました．
ユースケース-4は，プロキシサーバのみをモック化してHoverflyに担当してもらう場合が該当します．
具体的には，APIキーの差し替え，認証サーバへの通信などをHoverflyが担うことができます．

### ユースケース-5: 通信が想定通りかを監視する．

![スライド13.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/5522ddae-9567-0343-2686-ccedd98b8344.png)

HTTP(S)通信が想定通りに行われているかを判定するために利用します．
ユースケース-1~4ではHTTP(S)トラフィックのどこかでHoverflyによって処理が行われていましたが，ユースケース-5はトラフィックに対して一切干渉しません．

## 各ユースケースを実現するHoverflyのモード

Hoverflyでは以下の6つのモードを利用して，ウェブサーバやプロキシサーバとして振舞うことで前節のユースケースを実現することができます．

* Capture mode
* Simulate mode
* Spy mode
* Synthesize mode
* Modify mode
* Diff mode

|  | Capture | Simulate | Spy | Synthesize | Modify | Diff |
|----|:----:|:----:|:----:|:----:|:----:|:----:|
| ユースケース-1 |〇| 〇 |  |  |  |  |
| ユースケース-2 | 〇 | 〇 |  | △ |  |  |
| ユースケース-3 | 〇 |  | 〇 |  |  |  |
| ユースケース-4 | △ |  |  |  | 〇 |  |
| ユースケース-5 | △ |  |  |  |  | 〇 | 

### Capture mode

![スライド0.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/3a9b1341-d1a7-b697-fdee-ad8eae113d46.png)

Hoverflyをプロキシサーバとして起動し，Clientからのrequestと"外部サービス"からのresponseをキャプチャします．
ここでキャプチャした通信を元に`Simulations`を構成してくれます．
ステートフルなresponseも対応しており，利用したい場合は`--stateful`オプションをつけてCapture modeに設定します．
この機能によって**現在実現されているrequest-response関係については，即座にAPIサーバを構築できることを意味します．**

### Simulate mode

![スライド1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/9e94baa7-63fd-1d66-1927-4efe70b01765.png)

Hoverflyをプロキシサーバまたはウェブサーバとして起動します．
Hoverflyは，requestを受け取り`Simulations`を基づいてresponseを返すテスト用APIサーバとなります．
加えて，ユーザが作成した実行ファイル(`Middleware`)でresponseに操作を加えることもできます．
"外部サービス"への通信は一切届きません．

### Spy mode

![スライド2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/b6f86d3b-9590-7b23-88c6-1b4da1fc2f6e.png)

Hoverflyをプロキシサーバとして起動します．
`Simulations`によって"外部サービス"で処理するトラフィックとHoverflyで処理するトラフィックを判定する，テスト用APIサーバとなります．
"外部サービス"で処理する場合は，トラフィックに一切干渉しません．
`Middleware`でresponseに操作を加えることもできます．

### Synthesize mode

![スライド3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/07f3197a-2f24-71ad-e9e1-447b3a6226bb.png)

Hoverflyをプロキシサーバまたはウェブサーバとして起動します．
Hoverflyは，requestを受け取りresponseを返すテスト用APIサーバとなります．
Simulate modeと異なり，`Simulations`に紐づいたresponseを採用することなく，直接`Middleware`の内容を返答します．
"外部サービス"への通信は一切届きません．

### Modify mode

![スライド4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/230f2822-45eb-8398-92ff-73667b0377c4.png)

Hoverflyをプロキシサーバとして起動します．
Hoverflyは，Client-"外部サービス"間で`Middleware`により処理されたrequestとresponseを転送する環境を提供します．
Capture modeと違って`Simulations`の作成は行いません．

### Diff mode

![スライド5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1997806/3232621f-51a6-4487-1fcf-0dddbfdbafcf.png)

Hoverflyをプロキシサーバとして起動します．
Client-"外部サービス"間通信にHoverflyを挟んで通信を監視します．
"外部サービス"から送られてくるresponseと，Hoverflyがrequestから想定したresponseを比較します．
比較結果はAPI(GET /api/v2/diff)を利用することで参照できます．
比較結果は，Hoverflyインスタンスが停止されるか明示的に消去されるまで保持されます．

## 処理ロジックを定義する`Simulations`とマッチング検証

ここでは`Simulations`の中身と処理について説明します．
`Simulations`は，送られてきたrequestを評価するための比較条件，条件に合致したときに返答するresponse，responseを遅らせるlatencyなどの設定をまとめて保持します．
`Request Matchers(request-response pairs)`, `Global Actions(Delays)`, `Meta`で構成されます．
役割は次の通り．

| 名称 | 役割 |
|----|----|
| `Request Matchers -request` | request検証に用いる条件を定義 |
| `Request Matchers -response` | request検証条件に紐づいたresponseを定義 |
| `Global Actions - Delays` | `Simulations`内で共通でlatencyを定義 |
| `Meta` | `Simulations`の管理情報を記載 |

例として，ステートレスなCapture modeにて`https://www.google.com`に`curl`した際に生成された`Simulations`を以下に示します．
辞書と配列の組み合わせ構造を持つことで，複数の設定を並列して記載できるようになっていることがわかると思います．
(解説のために`//`でコメントを入れていますが，実際の`Simulations`には含まれません．)

```JSONC
{
"data": {
        "pairs": [  // 複数のrequest-responseペアを登録可能
                 {
                 "request": { // pair[0]のrequestを定義
                            "path": [ // requestの"path"フィールドを検証
                                    {
                                    "matcher": "exact", // 完全な一致
                                    "value": "/"
                                    }
                                    ],
                            "method": [ // requestの"method"フィールドを検証
                                      {
                                      "matcher": "exact", // 完全な一致
                                      "value": "GET"
                                      }
                                      ],
                            "destination": [ // requestの"destination"フィールドを検証
                                           {
                                           "matcher": "exact", // 完全な一致
                                           "value": "www.google.com"
                                           }
                                           ],
                            "scheme": [ // requestの"scheme"フィールドを検証
                                      {
                                      "matcher": "exact", // 完全な一致
                                      "value": "https"
                                      }
                                      ],
                            "body": [ // requestの"body"フィールドを検証
                                    {
                                    "matcher": "exact", // 完全な一致
                                    "value": ""
                                    }
                                    ]
                            },
                 "response": { // pair[0]のrequestに紐づくresponseを定義
                             "status": 200,
                             "body": "\u003c!doctype html\u003e\u003chtml itemscope=...",
                             "encodedBody": false, // responseのbodyにバイナリデータを設定可能
                             "headers": {
                                        "Alt-Svc": [
                                                   "h3=\":443\"; ma=2592000,..."
                                                   ],
                                        "Cache-Control": [
                                                         "private, max-age=0"
                                                         ],
                                        "Content-Type": [
                                                        "text/html; charset=ISO-8859-1"
                                                        ],
                                        "Date": [
                                                "Mon, 13 Dec 2021 01:35:02 GMT"
                                                ],
                                        "Expires": [
                                                   "-1"
                                                   ],
                                        "Hoverfly": [
                                                    "Was-Here"
                                                    ],
                                        "P3p": [
                                               "CP=\"This is not a P3P policy! See ..."
                                               ],
                                        "Server": [
                                                  "gws"
                                                  ],
                                        "Set-Cookie": [
                                                      "1P_JAR=2021-12-13-01; expires=Wed, ..."
                                                      ],
                                        "X-Frame-Options": [
                                                           "SAMEORIGIN"
                                                           ],
                                        "X-Xss-Protection": [
                                                            "0"
                                                            ]
                                        },
                             "templated": false // templateによる動的response構成の有化/無効
                             }
                 }
                 ],
        "globalActions": { // このSimulationsを使用するときに模擬するlatencyを設定
                         "delays": [], // 秒数でlatencyを設定
                         "delaysLogNormal": [] // 対数正規分布の乱数でlatencyを設定
                         }
        },
"meta": { // このSimulationsを管理するためのメタ情報を記入
        "schemaVersion": "v5.1",
        "hoverflyVersion": "v1.3.0",
        "timeExported": "2021-12-13T10:41:57+09:00"
        }
}
```

**1つの`Simulations`は複数のRequest Matchersを`pairs`配列に持つことで応答を分岐させる**ことができます．
（ステートフルな応答を設定する場合もrequest対responseは常に1対1です．この場合は`transitionsState`, `removesState`, `requiresState`, `sequence`などのキーを利用して管理します．）

トラフィック上で送られてきたrequestのフィールドに対しては，request定義内の各要素でマッチング検証を行うので，ここではこの要素を検証フィールドと呼ぶことにします．
検証フィールドは`matcher`タイプと`value`で構成されます．
`matcher`タイプは以下の3種類があり，それぞれ`value`に対してルールに基づいて一致評価します．

| `matcher`タイプ | 評価方法 |
|----|----|
| `exact` | 完全な一致 |
| `glob` | "\*"を利用したあいまい検索による一致 |
| `regex` | 正規表現に対する一致 |

**request定義の検証フィールドを適宜間引くことで応答を分岐させる**こともできます．

Hoverflyのデフォルト動作では，送られてきたrequestに対して`Simulations`の全request定義内の全検証フィールドを確認し，**検証結果が`true`となる検証フィールド数が最も多いrequest定義に紐づいたresponseを選択して返答**します．

templateを利用すると，送られてきたrequestの内容を元に動的にresponseを構成することもできます．
例えば，リクエストパラメータをresponseに含めて返答してほしい場合に活用できます．
また，Helper Methodsを使って逐次算出される乱数を，メールアドレスやIPアドレスなどの形に整形して含めることもできます．

`Simulations`においては次の2種類の方法でlatencyを設定できます．

* `globalActions`を使用して，`Simulations`で共通のlatencyを設定する．
* 各response定義にそれぞれlatencyを設定する．

また，トラフィック全体については後述する`Middleware`を利用してlatencyを設定することもできます．

実際の運用では，**Capture modeで生成した`Sumulations`を`simulation.json`にexportして頒布したり，自由に編集した`simulation.json`をimportしてSimulate modeやSpy modeで使ったりします．**

## URLフィルタリングによるHoverflyの利用制御

特定のhostやpathに対してHoverflyを動作させるかどうかの制御は，CLIから設定することができます．
指定には正規表現を含む文字列を使用できます．
またこのフィルタリングは全てのmodeで有効です．

## `Middleware`によるトラフィック操作

Client-"外部サービス"間，またはClient-Hoverfly間のデータ操作は，`Simulations`以外にも**任意**の言語のコード(Middleware)によって実現できます．
コードとresponseの受け渡しは標準入力/標準出力を利用することで，`Simulations`をむやみに変更しないようになっています．
したがって，コードの実行環境が存在することと標準入力/標準出力で受け渡しできる環境であることが利用条件になります．

## Python, JavaとHoverflyの連携

Python，Javaで開発している場合は，簡単にHoverflyを利用することができます．

* Python - [HoverPy](https://docs.hoverfly.io/projects/hoverpy/en/latest/)
* Java - [Hoverfly Java](https://docs.hoverfly.io/projects/hoverfly-java/en/latest/)

## request-responseのキャッシュ機能

Hoverflyは，送られてきたrequestに対してマッチング検証を行うことで適切なresponseを選択しています．
しかし，送られてきたrequestに対して毎回検証を行うと，responseの選択と応答に時間がかかってしまいます．
そのため，Hoverflyは一度送られてきたrequestに対しては，選択されたresponseとともにキャッシュに残す機能があります．
ここでrequestヘッダは時間やclientによって変化しやすいので，デフォルトでは含まれません．
`Simulations`が編集されたとき，キャッシュはリセットされます．

## おわりに

いかがでしたでしょうか．
私自身初めてAPIシミュレーションツールを触ってみたのですが，思っていた以上に応用範囲が広そうだと感じました．
簡単にテスト用APIサーバを立てられる点を利用すれば，大規模開発でスタブとして使うだけでなく，ハッカソンでのモック作成でもきっと活躍してくれることだろうと思いました．

ということで，明日のAdvent Calenderでは『フロントエンド/バックエンドを分離して開発するためにHoverflyを利用してみる』を投稿させていただこうと思います．

【追記 2021/12/19】
公開完了しました！

https://qiita.com/caunu-s/items/f5d3ded321a6f98706e0

Windows環境にてHoverflyの活用を試みています．
HTTPSを利用する具体的な方法もこちらに書かせていただきました．

ぜひ皆様のご参考になればと思います．