TL; DR
-
*recorder.AddHook
でカセットを加工する関数を登録
はじめに
go-vcrはHTTPリクエストを行う関数のテストに使用するライブラリです。
初回テストでは実際にAPIリクエストを発生させ、リクエスト、レスポンスの組み合わせをカセット(実体はyamlファイル)に保存します。
2回目以降はクライアントがカセットをもとにレスポンスを返すため、実際にはリクエストが発生しません1。
これにより、
- 実際のAPIリクエスト、レスポンスでテストできる
- リクエストを発生させずにテストできる
を両立することができます。
テスト実行時に対象サーバーへの疎通性やリクエスト負荷等を気にする必要はありません。また、冪等性のないリクエストが含まれていても繰り返し実行可能です。
レスポンスの加工
ところで、テストでは実際のレスポンスをそのまま使うと都合が悪い場合もあります。
- 認証情報等コミットしたくない情報がカセットに載ってしまう
- 実際のリクエストでは発生しなかった特殊なエラーレスポンスに対応する異常系をテストしたい
記録したカセットを手で書き直すことでも対応可能ですが、vcrの hooks
を使えばより手軽に修正ができます。
hooksを使用
hooksは、リクエストやレスポンスをカセット(yamlファイル)に保存する前に加工する関数です。
// Authorizationヘッダを消すhook
hook := func(i *cassette.Interaction) error {
delete(i.Request.Headers, "Authorization")
return nil
}
// hookを追加し、レスポンスを受け取った後に適用されるようにする
r.AddHook(hook, recorder.AfterCaptureHook)
実例:httpbinのIPアドレス取得API
今回は実例として https://httpbin.org/ の IPアドレス取得APIを使用します。
GET /ip
は、リクエスト元の(グローバル)IPアドレスを返します。
# この端末のグローバルIPアドレスが返ってくる(以下はxで伏字にしている)
$ curl httpbin.org/ip
{
"origin": "xxx.xxx.xxx.xxx"
}
このAPIを呼び出す関数をgo-vcrを使用してテストする場合
- セキュリティ上IPアドレス(=開発環境のグローバルIPアドレス)を含むカセットをコミットしたくない
という問題があります。そこで、hookを使ってIPアドレスをローカルホストに置き換えます2。
まずはhookを作ります。パスが /ip
に一致したとき、レスポンスボディで 127.0.0.1
を返すようにします3。
func patchGetIPResponse(i *cassette.Interaction) error {
if strings.Contains(i.Request.URL, "/ip") {
i.Response.Body = `{"origin": "127.0.0.1"}`
}
return nil
}
そして、recorder生成時にこのhookを追加します。
func runRecorder(cassette string, hooks []func(*cassette.Interaction) error) (*recorder.Recorder, error) {
r, err := recorder.New(cassette)
// 受け取ったhookをすべて追加
for _, hook := range hooks {
r.AddHook(hook, recorder.AfterCaptureHook)
}
return r, err
}
func TestShowIPAddress(t *testing.T) {
// 上記のhooksを指定
hooks := []func(*cassette.Interaction) error{
patchGetIPResponse,
}
r, err := runRecorder("testdata/get_ip", hooks)
if err != nil {
t.Fatal(err)
}
defer r.Stop()
var out bytes.Buffer
err = ShowIPAddress(&out, r.GetDefaultClient())
if err != nil {
t.Error(err)
}
// レスポンスを書き換えてからカセットに記録しているため、assertionも `127.0.0.1` を使用した内容になる
expected := "IP Address: 127.0.0.1"
actual := out.String()
if expected != actual {
t.Errorf("%q != %q", expected, actual)
}
}
実装側も含めたコード全体については以下のリポジトリをご覧ください。