こんにちは
Goの記事第4弾です。
今回は、Goのテストについてです。
あまりノウハウが無く、手探りで現在のプロジェクトにテストを導入していったので、
他にも書き方はいろいろあるかもしれません。
前提
以下のフレームワークとパッケージを使用しています。
Goバージョン | 1.18 |
フレームワーク | gin |
パッケージ | testify |
testify
testifyパッケージを使っています。
主に以下の機能をよく使いました。
- assert
- suite
assertionが使えるようになる
suiteを作成して、suite単位のSetup, Teardown が使えるようになる
Goのテストは標準パッケージだけで十分実装可能ですが、assertionがありません。
横着してassertionが使えるtestifyも使ってみました。
Why does Go not have assertions?
gin
フレームワークはginを使用しています。
テストでは主にモックサーバの立ち上げとルーティングに関係してきます。
基本的な形
Table Driven Tests
Goのテストでは、テーブル駆動テストで書く事がおすすめです。
境界値テストなどもキレイに書けます。
以下のような関数をテストするとして、テストコードの例を見ていきたいと思います。
func checkIntValueExample(i int64) bool {
if 0 < i && i < 10 {
return true
}
return false
}
1〜9の間の値であるか確認する簡単なものです。
testifyを使い、テストをこのように書く事ができると思います。
import (
"testing"
"github.com/stretchr/testify/suite"
)
// test suite
type ExampleSuite struct {
suite.Suite
}
// run test suite
func TestExampleSuite(t *testing.T) {
suite.Run(t, new(ExampleSuite))
}
func (s *ExampleSuite) TestCheckIntValueExample() {
cases := map[string]struct {
i int64
want bool
}{
"0": {0, false},
"1": {1, true},
"9": {9, true},
"10": {10, false},
}
a := s.Assert()
for name, v := range cases {
s.Run(name, func() {
matched := checkIntValueExample(v.i)
a.Equal(v.want, matched)
})
}
}
テーブル駆動テストで書くと、
入力と、期待する出力が一目で分かりやすくなります。
テストケースの追加も簡単です。
HTTPサーバをMockする
他のAPIサーバへのアクセスがある関数のテストを考えます。
標準のtestingパッケージでもmockサーバを用意できますが、
ここではginを使ったテストの書き方を見ていきます。
(以下のコードは例示のため簡略化しています。実際に動かしてないのでご注意ください)
// 指定のURLへリクエストしてレスポンス構造体を書き換える
func helloExample(url string, respbody interface{}) (int, error) {
req, _ := http.NewRequest(http.MethodGet, url, nil)
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
return http.StatusInternalServerError, err
}
defer res.Body.Close()
buf := &bytes.Buffer{}
tee := io.TeeReader(res.Body, buf)
if err = json.NewDecoder(tee).Decode(&respbody); err != nil {
return http.StatusInternalServerError, err
}
return res.StatusCode, nil
}
テストコード
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
)
// test suite
type HelloExampleSuite struct {
suite.Suite
testServer *httptest.Server
ctx *gin.Context
router *gin.Engine
}
// run test suite
func TestHelloExampleSuite(t *testing.T) {
suite.Run(t, new(HelloExampleSuite))
}
// handler
type helloResp struct {
Message string `json:"message"`
}
func testHelloHandler(c *gin.Context) {
resp := helloResp{
Message: "hello",
}
c.JSON(http.StatusOK, resp)
}
// setUp, tearDown
func (s *HelloExampleSuite) SetupSuite() {
s.ctx, s.router = gin.CreateTestContext(httptest.NewRecorder())
s.ctx.Request = httptest.NewRequest("GET", "/", nil)
s.router.GET("/get", testHelloHandler)
s.testServer = httptest.NewServer(s.router)
}
func (s *HelloExampleSuite) TearDownSuite() {
s.testServer.Close()
}
// test
func (s *HelloExampleSuite) TestHelloExample() {
cases := map[string]struct {
url string
status int
message string
}{
"statusOK": {"/get", 200, "hello"},
}
a := s.Assert()
for name, v := range cases {
s.Run(name, func() {
var resp helloResp
httpStatus, err := helloExample(s.testServer.URL+v.url, &resp)
if err != nil {
s.T().Fatal(err.Error())
}
a.Equal(v.status, httpStatus)
a.Equal(v.message, resp.Message)
})
}
}
立ち上げたサーバの情報は、suite構造体に持っておくと便利です。
Setup, Teardownはsuiteごと、テスト関数ごとに設定する事が可能です。
実行順序の例(Test1, Test2の2つのテストがある場合)
SetupSuite
SetupTest
Test1
TeardownTest
SetupTest
Test2
TeardownTest
TeardownSuite
今回はSetupSuiteで`gin.CreateTestContext`でモックサーバを立ち上げ、ルーティングを設定しています。
以上です。
今回はGoのテストの書き方について考えてみました。
ご参考になれば幸いです。
また他にも、DBアクセスがある関数をテストしたいケースもあると思います。
既存の環境のDBを使うのもなぁと困りましたが、
私はDockerにテスト用のDBを用意することで解決しました。
これについても今後振り返っていければと思います。
ご覧いただきありがとうございました。