ソフトウェア エンジニアであれば、外部 HTTP サービスと対話するコードの作成に精通しているはずです。結局のところ、それは私たちが行う最も一般的なことの 1 つです。データの取得、プロバイダーとの支払い処理、ソーシャル メディア投稿の自動化など、当社のアプリケーションにはほぼ常に外部 HTTP リクエストが含まれます。ソフトウェアの信頼性と保守性を高めるには、これらのリクエストを実行し、発生する可能性のあるエラーを処理するコードをテストする方法が必要です。これにより、いくつかのオプションが残ります:
これらのオプションは、特にすべて一緒に使用できる場合には、それほどひどいものではありませんが、VCR テストというより良いオプションがあります。
VCR テストは、ビデオカセット レコーダーにちなんで名付けられ、実際のリクエストからテスト フィクスチャを生成する模擬テストの一種です。フィクスチャはリクエストとレスポンスを記録し、将来のテストで自動的に再利用します。動的な時間ベースの入力を処理したり資格情報を削除したりするために、後でフィクスチャを変更する必要がある場合がありますが、モックを最初から作成するよりもはるかに簡単です。 VCR テストにはさらにいくつかの利点があります:
VCR テストの背後にある動機がわかったので、dnaeon/go-vcr を使用して Go で VCR テストを実装する方法をさらに深く掘り下げてみましょう。
このライブラリは、あらゆる HTTP クライアント コードにシームレスに統合されます。クライアント ライブラリ コードで *http.Client またはクライアントの http.Transport の設定がまだ許可されていない場合は、ここで追加する必要があります。
詳しくない方のために説明すると、http.Transport は http.RoundTripper の実装であり、基本的にはリクエスト/レスポンスにアクセスできるクライアント側のミドルウェアです。これは、500 レベルまたは 429 (レート制限) 応答に対する自動再試行を実装したり、メトリクスを追加したり、リクエストに関するログを記録したりする場合に役立ちます。この場合、go-vcr はリクエストを独自のインプロセス HTTP サーバーに再ルーティングできます。
簡単な例から始めましょう。無料の https://cleanuri.com API にリクエストを行うパッケージを作成したいと考えています。このパッケージは 1 つの関数を提供します: Shorten(string) (string, error)
これは無料の API なので、サーバーに直接リクエストを送信してテストできるでしょうか?これは機能する可能性がありますが、いくつかの問題が発生する可能性があります:
それでは、インターフェイスを作成してモックしたらどうなるでしょうか?私たちのパッケージは非常にシンプルなので、これでは非常に複雑になってしまいます。私たちが使用する最下位レベルのものは *http.Client なので、それを中心に新しいインターフェイスを定義し、モックを実装する必要があります。
もう 1 つのオプションは、httptest.Server によって提供されるローカル ポートを使用するようにターゲット URL をオーバーライドすることです。これは基本的に go-vcr の機能の簡略化されたバージョンであり、単純な場合には十分ですが、より複雑なシナリオでは保守できません。この例でも、生成されたフィクスチャの管理が、さまざまなモックサーバー実装を管理するよりもいかに簡単であるかがわかります。
私たちのインターフェースはすでに定義されており、https://cleanuri.com で UI を試してみることで有効な入出力がわかっているので、これはテスト駆動開発を実践する絶好の機会です。まず、Shorten 関数の簡単なテストを実装します。
package shortener_test func TestShorten(t *testing.T) { shortened, err := shortener.Shorten("https://dev.to/calvinmclean") if err != nil { t.Errorf("unexpected error: %v", err) } if shortened != "https://cleanuri.com/7nPmQk" { t.Errorf("unexpected result: %v", shortened) } }
とても簡単です! shorter.Shorten が定義されていないため、テストがコンパイルに失敗することはわかっていますが、とにかく実行するので、修正する方が満足度が高くなります。
最後に、この関数を実装してみましょう:
package shortener var DefaultClient = http.DefaultClient const address = "https://cleanuri.com/api/v1/shorten" // Shorten will returned the shortened URL func Shorten(targetURL string) (string, error) { resp, err := DefaultClient.PostForm( address, url.Values{"url": []string{targetURL}}, ) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("unexpected response code: %d", resp.StatusCode) } var respData struct { ResultURL string `json:"result_url"` } err = json.NewDecoder(resp.Body).Decode(&respData) if err != nil { return "", err } return respData.ResultURL, nil }
これでテストに合格しました!約束通り満足です。
VCR の使用を開始するには、Recorder を初期化し、テストの開始時に shorter.DefaultClient をオーバーライドする必要があります。
func TestShorten(t *testing.T) { r, err := recorder.New("fixtures/dev.to") if err != nil { t.Fatal(err) } defer func() { require.NoError(t, r.Stop()) }() if r.Mode() != recorder.ModeRecordOnce { t.Fatal("Recorder should be in ModeRecordOnce") } shortener.DefaultClient = r.GetDefaultClient() // ...
テストを実行して、テストのリクエストとレスポンスの詳細を含む fixtures/dev.to.yaml を生成します。テストを再実行すると、サーバーに接続する代わりに、記録された応答が使用されます。私の言葉をそのまま鵜呑みにしないでください。コンピューターの WiFi をオフにして、テストを再実行してください!
また、go-vcr は応答期間を記録して再生するため、テストの実行にかかる時間が比較的一定していることに気づくかもしれません。 YAML のこのフィールドを手動で変更して、テストを高速化できます。
この種のテストの利点をさらに実証するために、レート制限による 429 応答後の再試行という別の機能を追加しましょう。 API のレート制限は 1 秒あたりであることがわかっているため、Shorten は自動的に 1 秒待機し、429 応答コードを受信した場合に再試行できます。
API を直接使用してこのエラーを再現しようとしましたが、レート制限を考慮する前にキャッシュからの既存の URL で応答しているようです。今度は、偽の URL でキャッシュを汚染するのではなく、独自のモックを作成できます。
フィクスチャはすでに生成されているため、これは簡単なプロセスです。 fixtures/dev.to.yaml を新しいファイルにコピー/ペーストした後、成功したリクエスト/レスポンスのインタラクションを複製し、最初のレスポンスのコードを 200 から 429 に変更します。このフィクスチャは、レート制限の失敗後の成功した再試行を模倣します。
このテストと元のテストの唯一の違いは、新しいフィクスチャのファイル名です。 Shorten がエラーを処理する必要があるため、期待される出力は同じです。これは、テストをループにスローしてより動的にすることができることを意味します:
func TestShorten(t *testing.T) { fixtures := []string{ "fixtures/dev.to", "fixtures/rate_limit", } for _, fixture := range fixtures { t.Run(fixture, func(t *testing.T) { r, err := recorder.New(fixture) if err != nil { t.Fatal(err) } defer func() { require.NoError(t, r.Stop()) }() if r.Mode() != recorder.ModeRecordOnce { t.Fatal("Recorder should be in ModeRecordOnce") } shortener.DefaultClient = r.GetDefaultClient() shortened, err := shortener.Shorten("https://dev.to/calvinmclean") if err != nil { t.Errorf("unexpected error: %v", err) } if shortened != "https://cleanuri.com/7nPmQk" { t.Errorf("unexpected result: %v", shortened) } }) } }
またしても、新しいテストは失敗します。今回は未処理の 429 レスポンスが原因なので、テストに合格するために新機能を実装しましょう。シンプルさを維持するために、この関数は最大再試行と指数バックオフを考慮する複雑さを処理するのではなく、time.Sleep と再帰呼び出しを使用してエラーを処理します。
func Shorten(targetURL string) (string, error) { // ... switch resp.StatusCode { case http.StatusOK: case http.StatusTooManyRequests: time.Sleep(time.Second) return Shorten(targetURL) default: return "", fmt.Errorf("unexpected response code: %d", resp.StatusCode) } // ...
もう一度テストを実行して、テストが成功することを確認してください。
ご自身でさらに一歩進んで、不正なリクエストのテストを追加してみてください。不正なリクエストは、my-fake-url などの無効な URL を使用したときに発生します。
この例 (および不正なリクエストのテスト) の完全なコードは、Github で入手できます。
VCR テストのメリットは、この単純な例だけでも明らかですが、リクエストとレスポンスが扱いにくい複雑なアプリケーションを扱う場合には、さらに大きな影響があります。退屈なモックに対処したり、テストをまったく行わないことを選択したりするのではなく、独自のアプリケーションでこれを試してみることをお勧めします。すでに統合テストに依存している場合は、フィクスチャを生成できる実際のリクエストがすでに存在するため、VCR を始めるのはさらに簡単です。
パッケージの Github リポジトリでその他のドキュメントと例を確認してください: https://github.com/dnaeon/go-vcr
以上がGo での簡単な HTTP クライアント テストの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。