S.Hayakawaをフォローする

【Golang】Goのテストを書いてみよう

バックエンド

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

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

前提

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

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

testify

GitHub - stretchr/testify: A toolkit with common assertions and mocks that plays nicely with the standard library
A toolkit with common assertions and mocks that plays nicely with the standard library - 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

TableDrivenTests
The Go programming language. Contribute to golang/go development by creating an account on GitHub.

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