テスト #
関数のテストを書く #
Go 言語では組み込みの go test
コマンドでテストを実行する。
テスト用のファイルは xxx_test.go
という名前になり、xxx の部分はテスト対象のファイル名になる。
テスト用の関数は func TestXxx(t *testing.T)
という型になり Xxx の部分はテスト対象の関数名になる。
testing
はテスト用のパッケージであり、テストの成功失敗の判定やログの出力を行う。
下記のコードは絶対値を返す math.Abs
のテストをするものである。
テスト用のアサーションなどは用意されておらず、普通の Go のコードとして期待値と実測値を出して if で判定して、失敗したら T.Errorf
で失敗であると扱う。
package main
import (
"math"
"testing"
)
func TestAbs(t *testing.T) {
actual := math.Abs(-1)
expected := 1.
if actual != expected {
t.Errorf("Abs(-1) = %f; want 1", actual)
}
}
参考ドキュメント: testing
テストを実行する #
関数のテストを書くで作成したテストを go test
コマンドで実行する。
$ go test -v ✘ 2
=== RUN TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok _/path/to/current/dir 0.005s
メソッドのテストをする #
メソッドのテストの関数名は func TestXxx_Yyy(t *testing.T)
とする。
Xxx は型名で Yyy はメソッド名である。
package main
import (
"testing"
)
type S1 struct{}
func (s *S1) Hello() string {
return "Hello World"
}
func TestS1_Hello(t *testing.T) {
s := &S1{}
actual := s.Hello()
expected := "Hello World"
if actual != expected {
t.Errorf("failed. expected:%s, actual:%s", expected, actual)
}
}
参考ドキュメント: testing
テスト失敗時にテストを継続する・テストを即時終了する #
テストが失敗した時にそのテストの続きを実行するか、即時終了するかは使用する testing.T
のメソッドによって決まる。
func (c *T) Fail()
はテストを失敗扱いとし、その後の処理を継続する。
func (c *T) Errorf(format string, args ...interface{})
はログを出力しつつテストを失敗とし、その後の処理を継続する。
func (c *T) FailNow()
はテストを失敗扱いとし、その後の処理を行わずに強制終了する。
func (c *T) Fatalf(format string, args ...interface{})
はログを出力しつつテストを失敗とし、その後の処理を行わずに強制終了する。
package main
import (
"math"
"testing"
)
func TestAbs(t *testing.T) {
if v := math.Abs(0); v != 0 {
t.Fail()
}
if v := math.Abs(1); v != 1. {
t.FailNow()
}
if v := math.Abs(-1); v != 1. {
t.Errorf("failed. %f", v)
}
if v := math.Abs(-0.0001); v != 0.0001 {
t.Fatalf("failed. %f", v)
}
}
参考ドキュメント: testing
複数のテストデータでテストする #
複数のテストデータでテストをする場合、テーブル駆動テストという手法が用いられる。
テーブル駆動テストとは、テストデータや期待値をフィールドにもつ無名の構造体のスライスを宣言し即時で初期化することでテストデータテーブルを作り、 これを順次処理してテストを行う方法である。
package main
import (
"math"
"testing"
)
func TestAbs(t *testing.T) {
// テストデータのテーブルを定義
samples := []struct {
Input float64
Expected float64
}{
{Input: 0, Expected: 0},
{Input: 1, Expected: 1},
{Input: -1, Expected: 1},
}
// テーブルの各行ごとにテストを実行
for _, s := range samples {
if actual := math.Abs(s.Input); actual != s.Expected {
t.Errorf("Abs(%v) = %v, want %v", s.Input, actual, s.Expected)
}
}
}
参考ドキュメント: testing
テストを階層化する #
テスト関数の中でfunc (t *T) Run(name string, f func(t *T)) bool
を使うとテストを階層化できる。
package main
import (
"math"
"testing"
)
func TestAbs(t *testing.T) {
t.Run("zero", func(t *testing.T) {
if v := math.Abs(0); v != 0 {
t.Errorf("must zero. %v", v)
}
})
t.Run("one", func(t *testing.T) {
if v := math.Abs(1); v != 1 {
t.Errorf("must one. %v", v)
}
})
t.Run("minus one", func(t *testing.T) {
if v := math.Abs(-1); v != 1. {
t.Errorf("must one. %v", v)
}
})
}
参考ドキュメント: testing
テストの開始終了処理を行う #
サブテストを使ってテストの開始終了処理を実装できる。
package main
import (
"math"
"testing"
)
func TestAbs(t *testing.T) {
// テスト開始処理を実装
t.Log("start")
t.Run("success", func(t *testing.T) {
if v := math.Abs(0); v != 0 {
t.Errorf("must zero. %v", v)
}
})
t.Run("failed", func(t *testing.T) {
if v := math.Abs(-1); v != -1. {
t.Errorf("must one. %v", v)
}
})
// テスト終了処理を実装
t.Log("end")
}
参考ドキュメント: testing
パッケージ単位でテストの開始終了処理を行う #
パッケージ単位でテストの開始終了処理を行うためには func TestMain(m *testing.M)
という関数を利用する。
TestMain が定義されていると、go test
実行時に定義されたパッケージの個々のテストを呼び出す代わりに TestMain が実行される。
TestMain の中で M.Run
を実行すると同じパッケージの個々のテストが実行されるので、この前後で開始終了処理を行える。
例として testmain
パッケージの中で TestMain と個別のテストを実装し、go test の実行結果を見てみる。
testmain/main_test.go
には TestMain を定義し testmain/a_test.go
と testmain/b_test.go
には個々のテスト関数を定義する。
// testmain/main_test.go
package testmain
import (
"testing"
"os"
"log"
)
func TestMain(m *testing.M) {
// 開始処理
log.Print("setup")
// パッケージ内のテストの実行
code := m.Run()
// 終了処理
log.Print("tear-down")
// テストの終了コードで exit
os.Exit(code)
}
// testmain/a_test.go
package testmain
import (
"testing"
)
func TestA(t *testing.T) {
t.Log("TestA")
}
// testmain/b_test.go
package testmain
import (
"testing"
)
func TestB(t *testing.T) {
t.Log("TestB")
}
go test
の実行結果がこちら。テスト前後に開始終了処理に見立てたログが出ていることが確認できる。
$ go test -v
2021/10/20 16:41:48 setup
=== RUN TestA
--- PASS: TestA (0.00s)
a_test.go:6: TestA
=== RUN TestB
--- PASS: TestB (0.00s)
b_test.go:6: TestB
PASS
2021/10/20 16:41:48 tear-down
別ファイルのテストデータを読み込む #
別ファイルのテストデータは testdata
という名前のディレクトリに入れる。
testdata
に置かれたファイルは go test
や go build
などの各種ツールから無視されるので安全に取り扱うことができる。
以下のフィクスチャー用のファイルが testdata/fixture.json
として保存されているとする。
{
"test_cases": [
{"l": 1, "r": 1, "result": 2},
{"l": 0, "r": 0, "result": 0},
{"l": 0, "r": -1, "result": -1},
{"l": 1, "r": -3, "result": -2}
]
}
テストではこのようにフィクスチャーを読み込んで利用できる。
package main
import (
"encoding/json"
"io/ioutil"
"log"
"os"
"testing"
)
// フィクスチャーを読み込むための型の定義
type Fixture struct {
TestCases []testCase `json:"test_cases"`
}
type testCase struct {
L int `json:"l"`
R int `json:"r"`
Result int `json:"result"`
}
// TestMain を使ってテスト実行前にフィクスチャーを読み込んでテスト環境を用意する
func TestMain(m *testing.M) {
// フィクスチャーを読み込む
b, err := ioutil.ReadFile("testdata/fixture.json")
if err != nil {
log.Fatal(err)
}
f := new(Fixture)
if err := json.Unmarshal(b, f); err != nil {
log.Fatal(err)
}
// ここではログ表示しているだけだが利用しているデータストアへデータの登録をするなどできる
log.Printf("fixture: %#v", f)
// テストの実行
os.Exit(m.Run())
}
func TestA(t *testing.T) {
t.Log("fixture/a_test.go")
}
入出力を持つ関数のテストをする #
入出力が io.Reader
と io.Writer
として抽象化されている場合、
両方のインターフェイスの実装である bytes.Buffer
を利用するとテストに便利である。
package main
import (
"bytes"
"io"
"testing"
)
// io.Reader と io.Writer による入出力のある関数のテストをする.
func TestReaderWriter(t *testing.T) {
// テスト用の io.Reader 実装を作成する.
// io.Reader のモックは bytes.Buffer を使ってテストするのが手軽.
text := "string from input buffer"
input := bytes.NewBufferString(text)
// テスト用の io.Writer 実装を作成する.
// io.Writer のモックも bytes.Buffer を利用できる.
output := new(bytes.Buffer)
// io.Copy(io.Writer, io.Reader) を実行してテキストをコピーする.
if _, err := io.Copy(output, input); err != nil {
t.Error(err)
}
// テキストがコピーできていることを確認する.
if output.String() != text {
t.Errorf("failed to copy. output:%v", output)
}
}
参考ドキュメント: testing
HTTP ハンドラーをテストする #
HTTP ハンドラーの単体テストには net/http/httptest
を使う。
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
// ハンドラーを定義する
func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello %s", r.FormValue("name"))
}
// HelloHandler 単体でテストする
func TestHelloHandler(t *testing.T) {
// テスト用のリクエスト作成
req := httptest.NewRequest("GET", "http://example.com/?name=world", nil)
// テスト用のレスポンス作成
res := httptest.NewRecorder()
// ハンドラーの実行
HelloHandler(res, req)
// レスポンスのステータスコードのテスト
if res.Code != http.StatusOK {
t.Errorf("invalid code: %d", res.Code)
}
// レスポンスのボディのテスト
if res.Body.String() != "hello world" {
t.Errorf("invalid response: %#v", res)
}
t.Logf("%#v", res)
}
参考ドキュメント: testing , net/http/httptest
HTTP サーバーをテストする #
HTTP ハンドラー単体ではなく、ルーティングやミドルウェアなどの挙動も含めた HTTP サーバー全体の結合テストをしたい場合は httptest.Server
を使う。
package main
import (
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
)
type MyServer struct{}
func (s *MyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
m := http.NewServeMux()
m.HandleFunc("/hello", HelloHandler)
m.ServeHTTP(w, r)
}
func HelloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello %s", r.FormValue("name"))
}
// http.Handler からテスト用のサーバーを起動してサーバー単位でのテストを行う.
// ハンドラー単体のテストと比べてルーティング設定やミドルウェアの挙動などもテストできる.
func TestMyServer_ServeHttp(t *testing.T) {
// テスト用のサーバーを起動
ts := httptest.NewServer(&MyServer{})
defer ts.Close()
// HelloHandler のテスト
res, err := http.Get(ts.URL + "/hello?name=world")
if err != nil {
t.Error(err)
}
if res.StatusCode != http.StatusOK {
t.Errorf("invalid response: %v", res)
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Error(err)
}
res.Body.Close()
if string(body) != "hello world" {
t.Errorf("invalid body: %s", body)
}
}
参考ドキュメント: testing , net/http/httptest