JavaでもVCRでHTTPテストのモックを生成したい [OkReplay]

## はじめに
この記事は [ニフクラ](https://www.nifcloud.com/) 等を提供している、[富士通クラウドテクノロジーズ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/fjct) の 17 日目の記事です。

昨日は @ktakaaki さんの [ITエンジニアがレザークラフトを真面目にやってみた](https://qiita.com/ktakaaki/items/7dd3268cd1b16bea2771) でした。
作品の完成度が高くて驚いています。また、完成イメージから設計図に落とし込む部分はやはり"ITエンジニアが"の部分も生かされているのでしょうか。

本日は、サービス間のテストに用いることができるVCRの仕組みをJavaエンジニアの皆様に向けて紹介したいと思います。

## サービス間のテスト
他サービスのAPI呼び出しを含むテストに用いる手法は大きく3つあるかと思います。
- 結合テスト(実際にAPIを呼び出す)
- モック
- CDC(コンシューマ駆動契約)テスト

マイクロサービス関連の記事ではCDCテストが取り上げられているのをよく見かけますが、導入には少しハードルが高いなと個人的には感じています。
VCRは結合テストとモックのちょうど中間のような仕組みであり、導入もテスト用のライブラリを１つ追加するだけなのでそれほど難しくなさそうです。

私はGoで [go-vcr](https://github.com/dnaeon/go-vcr) は活用しているのですが、同じようなものがJavaでも実現できないか探した際に便利そうなものがあったのでそちらについて紹介します。

## VCRとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245298/e9d0cf62-76ea-5745-83e2-56d1809c208a.png)

VCR[^1]とはHTTPのリクエスト/レスポンスをファイルに記録し、テストでモックとして再利用する仕組みです。
HTTPクライアントにVCRのレコーダー/プレイヤーを差し込み、テープ（ファイル）にすでに記録されているかどうかで以下のようにリクエスト先を切り替えます。
- 既知のHTTPリクエスト
  - テープからHTTPレスポンスを再生（モック）
- 未知のHTTPリクエスト
  - 実際にHTTPリクエストを行いテープに記録。

詳しい説明や導入のメリットについては他言語ですが以下の記事がわかりやすいです。 
[VCR in Go：モック自動生成で楽しちゃう話
](https://speakerdeck.com/yyh_gl/vcr-in-go-motukuzi-dong-sheng-cheng-dele-sitiyauhua) 個人的には、CI上でテストを流す際に実際のHTTPリクエストが発生しなくなることで、他サービスへの疎通経路を用意しなくてよいあたりが便利に感じます。

## Javaでも利用できるのか
VCRはRubyの [vcr/vcr](https://github.com/vcr/vcr) がオリジナルなのですが、他言語でも類似のライブラリが開発されています。   
Javaにおいては2022/12現在以下が見つかりました。
- [Betamax](https://github.com/betamaxteam/betamax) (:star: 459 最新コミット on May 28, 2017)
- [okreplay](https://github.com/airbnb/okreplay) (:star: 756 最新コミット on Jan 11, 2022)

私はHTTPクライアントとしてOkHttpに馴染みがあったので関連ライブラリであるOkReplayを試してみたいと思います。
(Betamaxは試していないのですが紹介記事はいくつかありそうでした！)

ちなみに、OkHttpやOkReplayはKotlinにも対応しています。

## OkReplayでテストを書く
テストの書き方をコードで説明していきます。最後にサンプルでGitHubリポジトリも載せましたので合わせてご確認ください。

### ライブラリの追加
まずはOkReplayを依存に追加します。gradleを使っていれば下記のように書きます。   
※ [README](https://github.com/airbnb/okreplay) には プラグイン `com.airbnb.okreplay:gradle-plugin` についても追加するように書いてありますが、これは任意で問題ないです。

```groovy:build.gradle
dependencies {
    testImplementation 'com.airbnb.okreplay:okreplay:1.6.0'
}
```

### InterceptorにOkReplay追加
VCRテストをするためにはOkReplayをOkHttpのクライアントに差し込む必要があります。OkHttpの機能である[Interceptor](https://square.github.io/okhttp/features/interceptors/) の仕組みを使って通信の前後に差し込みます。[^2]

以下の図におけるAPPLICATION INTERCEPRTORSの位置にOkReplayを割り込ませることで、リクエスト/レスポンスの記録や再生を可能にします。

[^3]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/245298/96394f84-420c-c60e-8b39-0771c7e18d26.png)
 

実装としては下記のように、OkHttpClientのビルド時にaddInterceptorで追加します。
```java
OkReplayInterceptor okreplayInterceptor = new OkReplayInterceptor();
OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(okreplayInterceptor) // 追加
    .build();
```
※ プロダクションコードで利用する際のポイントとしては、テスト以外のときはむしろVCRは設定したくないと思いますので、テスト時にOkHttpClientを外から変更可能にしておくと良いです。

簡単に書くと下記のような形にしておくイメージです。

```java
public class OkreplaySample {
    private OkHttpClient client;

    public OkreplaySample() {
        this.client = new OkHttpClient.Builder().build();
    }

    // テストではこちらを使う
    public OkreplaySample(OkHttpClient client) {
        this.client = client;
    }
}
```

### 補足: `@OkReplay` アノテーションの利用について
[README](https://github.com/airbnb/okreplay/blob/master/readme.md)では`@OkReplay`アノテーションを利用するように書いてあります。
これは `com.airbnb.okreplay:junit` の機能なのですが、junit5では利用できません。(junit5で廃止された`@Rule`を利用しているため) 

Android開発でAndroidJUnit4を使っている場合などはそのまま `@OkReplay` をテストメソッドに追加する方法も便利かと思います。 [サンプル](https://github.com/airbnb/okreplay/blob/master/okreplay-sample/src/androidTest/java/okreplay/sample/ExampleInstrumentedBarTest.java)

アノテーションを使わないやり方を以降説明していきます。

### OkReplayの設定
OkReplayの設定をします。ポイントは`interceptor()` で先ほどクライアントに差し込んだokreplayInterceptorをそのまま設定に追加することです。

他には、生成先のフォルダや、マッチルール（リクエストを構成するものの中で何が同じであれば既知のリクエストとするか）が設定できます。

```java
// ↑ Interceptorの生成
OkReplayConfig configuration = new OkReplayConfig.Builder()
			.defaultMode(TapeMode.READ_WRITE)
            .defaultMatchRule(
                    ComposedMatchRule.of(MatchRules.method, MatchRules.uri, MatchRules.body))
            .interceptor(okreplayInterceptor)
            .build();
```

### 記録の開始、終了
最後に、Recorderのstartとstopをリクエストの前後に追加します。stopをしないとカセットの保存が行われないので注意です。

```java
// ↑ OkReplayConfigの生成
Recorder rec = new Recorder(configuration);
rec.start("sample"); // テープ名を指定

// リクエストが発生するテストコード

rec.stop();
```

### テスト実行
ここまでで準備は完了です。

あとはテストを実行すれば下記のようなテープが生成されます。

1回目は実際にリクエストが発生し、2回目からはこのテープがレスポンスとして読まれます。

```yaml:src/test/resources/okreplay/tapes/sample.yaml
!tape
name: test
interactions:
- recorded: 2022-12-16T05:16:17.011Z
  request:
    method: GET
    uri: https://httpbin.org/get
  response:
    status: 200
    headers:
      access-control-allow-credentials: 'true'
      access-control-allow-origin: '*'
      content-length: '267'
      content-type: application/json
      date: Fri, 16 Dec 2022 05:16:15 GMT
      server: gunicorn/19.9.0
    body: "{\n  \"args\": {}, \n  \"headers\": {\n    \"Accept-Encoding\": \"gzip\", \n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"okhttp/4.10.0\", \n    \"X-Amzn-Trace-Id\": \"Root=1-639bff1f-23f2648b6e189b60213400db\"\n  }, \n  \"origin\": \"x.x.x.x\"\
      , \n  \"url\": \"https://httpbin.org/get\"\n}\n"

```

### サンプル
以上のものをまとめたサンプルを記載します。
Recoderの開始を `@BeforeEach` で行うなど多少のリファクタをしています。

https://github.com/tanopanta/okreplay-sample/blob/master/src/test/java/sample/OkreplaySampleTest.java

## Tips
### テープ記録時にはログが出力される
テープが出力されない場合などは、ログを見ると状況が確認できます。   
たとえば記録している想定の時に `INFO: writing tape` のログがない場合はrecoderのstopを忘れている可能性があります。

```sh
Dec 16, 2022 10:30:50 PM okreplay.OkReplayInterceptor intercept
WARNING: no matching request found on tape 'testGet()' for request GET https://httpbin.org/get
Dec 16, 2022 10:30:52 PM okreplay.OkReplayInterceptor recordResponse
INFO: Recording request GET https://httpbin.org/get to tape 'testGet()'
Dec 16, 2022 10:30:55 PM okreplay.YamlTapeLoader writeTape
INFO: writing tape testGet() to file testGet.yaml...
```

### 止むを得ずテープのYAMLを手で書きなおしたい場合は修正箇所をコメントしておくとよい
自動生成のものを手動で書き換えるのはアンチパターンとも言えますが、例えば異常系のテストを実現するために一部手動で書き換えたいなどのケースもあるかと思います。
そういった場合はYAMLに修正点のコメントを書くと良いと思います。
例）
```yaml
  response:
    status: 500 # FIX: 200から変更
```
コメントにはなにか決まった文字列(`FIX:` など) を入れるルールを作っておけばあとから修正点を見つけやすいです。

また、ファイル名も `fix_` のようなプレフィックスをつけておくと同様に見つけやすいです。

以上の工夫をしておくだけで、テープを取り直す必要が出た際の対応の難易度が変わってきます。


## まとめ

この記事は[富士通クラウドテクノロジーズ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/fjct)の17日目の記事でした。

VCRの仕組みを使えば継続的に実行可能なテストを簡単に追加していけるのでおすすめです。

明日は@yu_uniさんの「[パキラ君の成長日記（自動水やり編）](https://qiita.com/yu_uni/items/05de31877008059a534e)」です。
FJCTで自宅サーバーで自動化に取り組んでる方はいるのかと思いますが、植物の運用を自動化しているエンジニアの方もいるのですね！ 

明日の記事もお楽しみに👋。

[^1]: Video Cassette Recorder の略かと思われます。
[^2]: Interceptorは同じクライアントに複数設定可能で、もちろん自作も可能です。 [OkHttpのUserAgentにGitのタグバージョンを入れる](https://tanopanta.hatenablog.jp/entry/2022/09/13/013833) の記事も書きましたのでよかったら参考にしてください。
[^3]: 画像引用元: https://square.github.io/okhttp/features/interceptors/