Goでミドルウェアとインターセプターのテストをする方法
はじめに
この記事では HTTP のミドルウェアと gRPC のインターセプターのユニットテストの方法について紹介します。
- HTTPミドルウェアのテスト
- gRPCインターセプターのテスト
HTTP のミドルウェアや gRPC のインターセプターといえば Web サービスの共通処理の実装が集中する場所です。共通処理であるが故に開発初期に作り込んで後から手を入れることが少ないという特性があります。あまり手を入れることがないからといってユニットテストを省いてしまうと、あとから機能追加したり、バグを発見したりしたときに慌てることになります。共通処理が集まるコードはいざという時に備えてしっかりユニットテストをしてあげましょう。
HTTPミドルウェアのテスト
まずは HTTP ミドルウェアのテスト方法を見てみます。
テスト対象のミドルウェア
以下の HTTP ミドルウェアのコードをテストします。コンテキストにデータを追加するだけのシンプルなミドルウェアです。
// テスト対象のコード(Middleware)
func HTTPMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// コンテキストに値をセットする
ctx := context.WithValue(r.Context(), "foo", "bar")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
テストの実装
HTTP ミドルウェアのテストには httptest パッケージの NewRequest と NewRecorder を使います。これらを使うと擬似的な HTTP リクエストを作成することができます。テストコードは以下のようになります。
import (
"testing"
"net/http"
"net/http/httptest"
)
func TestHTTP(t *testing.T) {
mux := http.NewServeMux()
mux.Handle("/test1", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// テスト対象のミドルウェアが想定通りの値をセットしているかテストする
ctx := r.Context()
got := ctx.Value("foo")
if got != "bar" {
t.Errorf("got: %s, want: bar", got)
}
}))
// テスト対象のミドルウェアをセットする
mid := HTTPMiddleware(mux)
// 擬似的な HTTP リクエストを生成する
req1 := httptest.NewRequest(http.MethodGet, "/test1", nil)
rec1 := httptest.NewRecorder()
mid.ServeHTTP(rec1, req1)
}
gRPCインターセプターのテスト
つぎに gRPC インタセプターのテスト方法を見てみます。
テスト対象のインターセプター
以下の gRPC インターセプターのコードをテストします。先ほどの HTTP ミドルウェアと同じでコンテキストにデータを追加するだけのシンプルなインターセプターです。
// テスト対象のコード(Interceptor)
func UnaryServerInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// コンテキストに値をセットする
ctx = context.WithValue(ctx, "foo", "bar")
return handler(ctx, req)
}
テスト実装の前に少し前準備が必要です
gRPC の場合は HTTP と違いテスト用のサーバを立ち上げる必要があります。以下のように bufconn を使用してネットワーク接続不要なサーバを立ち上げます。
import (
"context"
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/interop"
pb "google.golang.org/grpc/interop/grpc_testing"
"google.golang.org/grpc/test/bufconn"
)
// これを使うとネットワーク接続なしでテストできる
// サーバ起動時以外に、サーバに接続する時にも使うのでグローバル変数として宣言します
var lis *bufconn.Listener
// テストサーバを立ち上げる
func serve(sOpt []grpc.ServerOption) *grpc.Server {
lis = bufconn.Listen(1024*1024)
s := grpc.NewServer(sOpt...)
// TestServiceServerを登録する
pb.RegisterTestServiceServer(s, interop.NewTestServer())
go func() {
if err := s.Serve(lis); err != nil {
panic(err)
}
}()
return s
}
テストの実装
テスト用サーバの準備ができたのでインターセプターのテストを実施します。テスト対象のインターセプターが正しくコンテキストに値をセットできているかを、テスト用のインターセプターを使って検証します。
// テスト関数
func TestGRPC(t *testing.T) {
// テストを実施するためのインターセプター
fn := func (
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// テスト対象のインターセプターが想定通りの値をセットしているかテストする
got := ctx.Value("foo")
if got != "bar" {
t.Errorf("got: %s, want: bar", got)
}
return handler(ctx, req)
}
sOpt := []grpc.ServerOption{
grpc.ChainUnaryInterceptor(
// テスト対象のインターセプター
UnaryServerInterceptor,
// テストを実施するインタセプター
fn,
),
}
// gRPCサーバを起動する
s := serve(sOpt)
defer s.Stop()
ctx := context.Background()
// ダイアル関数
dial := func(context.Context, string) (net.Conn, error) {
// lisはグローバルに宣言されている変数
return lis.Dial()
}
// gPRCコネクションの生成
conn, err := grpc.DialContext(
ctx,
"bufnet",
append([]grpc.DialOption{
grpc.WithContextDialer(dial),
grpc.WithInsecure(),
})...,
)
if err != nil {
t.Fatalf("fialed to dial: %v", err)
}
defer conn.Close()
// テスト用gRPCクライアントの生成
client := pb.NewTestServiceClient(conn)
// gRPCのメソッド呼び出し
interop.DoEmptyUnaryCall(client)
}
gRPC メドッドの実行について、grpc_testing パッケージにコンパイル済みの TestServiceServer が用意されているのでそちらを利用しました。interop パッケージの DoEmptyUnaryCall 関数を使って TestServiceServer のメソッドを呼び出すことができます。
interop パッケージには今回のテストで使用した DoEmptyUnaryCall 関数以外にも以下の gRPC メソッド呼び出し関数があります。
- DoLargeUnaryCall
- DoClientStreaming
- DoServerStreaming
- DoPingPong
詳細はこちらを参照してください。
まとめ
- HTTPのミドルウェアのテストにはnet/http/httptestを使う
- gRPCのインターセプターのテストにはgoogle.golang.org/grpc/interopを使う
- gRPCのインターセプターをテストするときはgRPCサーバを起動すること
今回紹介したテスト方法は単体でミドルウェア(インタセプター)のテストをするよりは、複数のミドルウェア(インターセプター)を組み合わせて疎通確認をするのに向いています。
Discussion