テスト

テスト #

関数のテストを書く #

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)
	}
}

play_circleRun open_in_newRun In The Playground

参考ドキュメント: 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)
	}
}

play_circleRun open_in_newRun In The Playground

参考ドキュメント: 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)
	}
}

play_circleRun open_in_newRun In The Playground

参考ドキュメント: 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)
		}
	}
}

play_circleRun open_in_newRun In The Playground

参考ドキュメント: 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)
		}
	})
}

play_circleRun open_in_newRun In The Playground

参考ドキュメント: 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")
}

play_circleRun open_in_newRun In The Playground

参考ドキュメント: 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.gotestmain/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 testgo 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.Readerio.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)
	}
}

play_circleRun open_in_newRun In The Playground

参考ドキュメント: 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)
}

play_circleRun open_in_newRun In The Playground

参考ドキュメント: 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)
	}
}

play_circleRun open_in_newRun In The Playground

参考ドキュメント: testing , net/http/httptest