[Golang]Goのテストを書いてみよう

こんにちは
Goの記事第4弾です。
今回は、Goのテストについてです。

あまりノウハウが無く、手探りで現在のプロジェクトにテストを導入していったので、
他にも書き方はいろいろあるかもしれません。

前提

以下のフレームワークとパッケージを使用しています。

 goバージョン  1.18 
 フレームワーク  gin
 パッケージ  testify 


 

testify

https://github.com/stretchr/testify

testifyパッケージを使っています。
主に以下の機能をよく使いました。

  • assert
  • suite

assertionが使えるようになる
suiteを作成して、suite単位のSetup, Teardown が使えるようになる

Goのテストは標準パッケージだけで十分実装可能ですが、assertionがありません。
横着してassertionが使えるtestifyも使ってみました。
Why does Go not have assertions?

gin

フレームワークはginを使用しています。
テストでは主にモックサーバの立ち上げとルーティングに関係してきます。

基本的な形

Table Driven Tests

https://github.com/golang/go/wiki/TableDrivenTests
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を用意することで解決しました。
これについても今後振り返っていければと思います。
ご覧いただきありがとうございました。

参考

テストしやすいGoコードのデザイン

Goテストモジュール Testifyをつかってみた

画像

renee french

前へ

JUnit5を使った自動テストを試してみた

次へ

docker+react+python+fastapiで簡単な開発環境をリバースプロキシを使用して構築する