見出し画像

Go でサーバのエンドツーエンドテストを行う方法

こんにちは、けんにぃです。ナビタイムジャパンで公共交通の時刻表を使ったサービス開発やリリースフローの改善を担当しています。

今回はサーバのエンドツーエンドテストを Go で実装しましたので、その方法についてまとめます。

テストを書く負担

弊社で稼働しているサーバアプリケーションの中にはパフォーマンス指向に実装された C++ 製のサーバがあります。

C++ はパフォーマンスが出やすい反面、ライブラリが少なくて不具合や実装コストが高いという問題もあります。

そのため C++ でテストを書くというのもそれなりのコストが掛かります。プロダクトの品質を担保するためのテストが上手く作れないという問題が起こってしまうのです。

テストにおけるプログラム言語選択

しかしテストを記述するのに、使用するプログラム言語は必ずしもプロダクト開発で使用するものと、同じである必要はないかなと思います。

例えば REST API のレスポンスをテストする場合は HTTP を扱える言語であればどの言語でも実装できます。

エンドツーエンドテストはインターフェースが特定の言語に特化したものにはならないため、プロダクトとは別の言語を導入しやすいのではないかと思います。

Go でサーバのテストを書く

以上のことから C++ 製サーバのエンドツーエンドテストを Go で書くことにしました。他の言語がある中、Go を選択した理由は、

・速い
・HTTP などのサーバ系の機能を扱いやすい
・いずれ C++ のシステムを置き換える際に Go を選択肢にできそう

などが挙げられます。

ちなみに C++ の代替という意味では Rust も候補に挙げられます。Rust を選択しなかった理由は、下記のようなものが挙げられます。

・今回説明するテスト手法は C++ 開発以外でも適用できる
・サーバ開発者は Rust よりも Go に興味を持つ人の方が多い
・そのため Go を学んでもらう方が応用範囲が広がる

サーバより下層のコアな部分の開発であれば、テスト実行自体でパフォーマンスが出ないという問題が起こることもあり、そういう場合は Rust の方が向いているかも知れません。実際、弊社ではそのような問題に対応するため Rust 製のツールもいくつか開発しています。

実装では httpexpect というテストフレームワークを使用しました。

httpexpect は HTTP のエンドツーエンドを行うためのフレームワークです。

httpexpect の使い方

適当な HTTP サーバを使ってテストを書きたいので httpbin というモックサーバを使用します。

http://httpbin.org/json にアクセスすると下記のような JSON が返却されます。

{
 "slideshow": {
   "author": "Yours Truly", 
   "date": "date of publication", 
   "slides": [
     {
       "title": "Wake up to WonderWidgets!", 
       "type": "all"
     }, 
     {
       "items": [
         "Why <em>WonderWidgets</em> are great", 
         "Who <em>buys</em> WonderWidgets"
       ], 
       "title": "Overview", 
       "type": "all"
     }
   ], 
   "title": "Sample Slide Show"
 }
}

この JSON を使ってテストを書いてみようと思います。

httpbin_test.go

package httpbin

import (
	"net/http"
	"testing"

	"github.com/gavv/httpexpect/v2"
)

func TestHttpBinJson(t *testing.T) {
	e := httpexpect.New(t, "http://httpbin.org")

	// GET http://httpbin.org/json をリクエスト
	e.GET("/json").
		Expect().              // レスポンスを取得
		Status(http.StatusOK). // HTTP ステータスコードが OK であること
		JSON().                // レスポンスボディが JSON であること
		Object()               // JSON がオブジェクトであること
}

JSON の中身についてテストしたい場合は Object() の結果を受け取ってテストできます。

r := e.GET("/json").
	Expect().
	Status(http.StatusOK).
	JSON().
	Object()

// JSON が "slideshow" キーを持つこと
r.ContainsKey("slideshow")

テストの実行方法は Go 標準の単体テストを実行するときと同じです。

$ go test

httpexpect を使って下記のようなことをテストすることができます。

・HTTP ステータスコード
・HTTP ヘッダ
・JSON のキーの存在
・JSON の要素の値

JSONPath を使った要素参照

JSON の特定の要素を参照してその値についてテストをしたい場合は JSONPath という構文を使って要素参照を行います。

// slideshow.slides は配列であること
r.Path("$.slideshow.slides").Array()

$.slideshow.slides が JSONPath の構文になります。JSONPath の書き方は下記のリポジトリを参考にしてください。

JSON Schema を使った構造のバリデーション

JSON Schema は JSON の構造をスキーマとして定義するための仕様です。例えば

{
    "name": "Alice",
    "age": 25
}

のような JSON に対して JSON Schema を定義すると

{
   "type": "object",
   "properties": {
       "name": { "type": "string" },
       "age": { "type": "integer", "minimum": 0 }
   },
   "required": ["name"]
}

のように書くことができます。これは

・対象の JSON はオブジェクト型である
・"name" フィールドは文字列である
・"age" フィールドは 0 以上の整数である
・"name" フィールドは必ず存在し "age" フィールドは任意であること

というスキーマを定義しています。httpexpect はレスポンスの JSON が JSON Schema の定義にマッチしているかどうかをテストすることができます。

// レスポンス r は JSON Schema の定義に合致するか?
r.Schema(`
	{
		"type": "object",
		"properties": {
			"slideshow": {
				"type": "object",
				"properties": [
					"author": { "type": "string" }
					...
				]
			}
		}
	}
`)

JSON Schema を使うと下記のことがテストできるようになります。

・オブジェクトの構造
・フィールドの型
・フィールドの存在が必須か任意か
・文字列のフォーマット

Schema() メソッドは引数に JSON Schema を取得するための URL を指定することもできます。

// http://path/to/schema.json で JSON Schema を取得
r.Schema("http://path/to/schema.json")

この機能をテストで使用するため、サーバの API の 1 つとして JSON Schema を返す API を実装しました。

API の仕様そのもの(すなわち JSON Schema)を API として取得できるようにするという自己言及的な実装は、サーバのドキュメントを作成するコストを下げる効果もあるためオススメです。

CI でテストを実行する

Jenkins などの CI でテストを実行する場合はテストの実行結果のレポートを閲覧したくなると思います。こういうケースでは大抵 JUnit 形式のレポートファイルを生成するのですが go test コマンドはテストの実行結果を JUnit 形式で出力する機能を持っていないため gotestsum というツールを使用してレポートを作成します。

インストールは go get で行います。

$ go get gotest.tools/gotestsum

インストールされると gotestsum コマンドが使えるようになります。このコマンドを使うと JUnit 形式のレポートを出力できるようになります。

$ gotestsum --junitfile test-reports.xml

また gotestsum はテストの実行中のメッセージを色付けして標準出力に出してくれるため CI 上でテストを実行しない場合でも活用できます。

スクリーンショット 2020-07-07 20.21.15

まとめ

以上 Go でエンドツーエンドのテストを書いた話でした。Go はシンプルな記述ができて動作も速いため、新規のサーバを実装するときにもオススメしたいプログラム言語です。