注: 本記事は元々 Qiita にて公開していたものの再掲です。
概要
- testify
- golang でテストをより記述しやすくするためのパッケージ
- その中で
assert
パッケージは、結果値の妥当性をチェックする関数を提供する
本記事で使用したバージョン
- go: 1.16
- testify 1.7.0
テストの書き方と結果出力
import
import ( // ... 他の必要なパッケージ "github.com/stretchr/testify/assert" )
引数の指定
(例) Equal
関数
assert.Equal(t, expected, got, description)
- 1番目の引数は、
testing.T
- そのあと、テスト関数ごとに指定の引数を追加する
- 2つの値を比較するとき、大抵は
期待される値
実際の値
の順だが、たまに逆の関数もある - 指定の順番を間違えると、テスト失敗時の出力で逆に表示されてしまう
- 2つの値を比較するとき、大抵は
- 引数の末尾にテストの説明を任意で追加することができる。(なくてもよい)
type Member ( Name string Grade int ) func TestEqual(t *testing.T) { expected := &Member{Name: "Niko", Grade: 3} assert.Equal(t, expected, &Member{Name: "Niko", Grade: 2}, "%s is 3rd grade", expected.Name) }
結果表示
- テストに失敗すると、以下のような内容が出力される
=== RUN TestEqual assert_test.go:27: Error Trace: Error: Not equal: expected: &assertion_test.Member{Name:"Niko", Grade:2} actual : &assertion_test.Member{Name:"Niko", Grade:3} Diff: --- Expected +++ Actual @@ -2,3 +2,3 @@ Name: (string) (len=4) "Niko", - Grade: (int) 2 + Grade: (int) 3 }) Test: TestEqual Messages: Niko is 3rd grade --- FAIL: TestEqual (0.00s)
関数
以下の関数で、カッコ内でプレフィクス Not
または No
がついているものは、元のテストの逆の結果になることのテスト関数。
等価性チェック
Equal(NotEqual)
Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
- 実際値が期待値と一致していることをテストする
- 値はスカラー値だけでなく、構造体のインスタンス等でも比較可能
- 唯一、function 同士の比較はできない
type Member struct { Name string Grade int } func TestEqual(t *testing.T) { num := 111 str := "Honoka" arr := []string{"Kotori", "Umi", "Hanayo"} niko := &Member{ Name: "Niko", Grade: 3, } assert.Equal(t, 111, num) assert.NotEqual(t, 222, num) assert.Equal(t, "Honoka", str) assert.Equal(t, []string{"Kotori", "Umi", "Hanayo"}, arr) assert.Equal(t, &Member{Name: "Niko", Grade: 3}, niko, "%s is 3rd grade", niko.Name) }
EqualValues(NotEqualValues)
EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
- 2つの値が同一かどうかをテストする
Equal
との違いは、型が同一でなくても、型変換により同一とみなせる場合はテストが成功する点
func TestEqualValues(t *testing.T) { assert.EqualValues(t, uint(123), int(123)) }
Same(NotSame)
Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool
- 2つのポインタ値が同一オブジェクトを指していることをテストする
Same
であるためには以下の両方の条件を満たす必要がある- ポインタが同一オブジェクトを指している(ポインタ値が同一)
- 同一の型である
func TestSame(t *testing.T) { eri := &Member{ Name: "Eri", Grade: 3, } ptrEri := eri type Student *Member sEri := Student(eri) assert.Same(t, eri, ptrEri) assert.NotSame(t, &Member{Name: "Eri", Grade: 3}, ptrEri) // assert.Same(t, eri, sEri, "not same types") // fail }
- 一番下のテストは、ポインタは同一(
0xc0000b61f8
)だが、型が異なるため失敗する
assert_test.go:42: Error Trace: Error: Not same: expected: 0xc0000b61f8 &assertion_test.Member{Name:"Eri", Grade:3} actual : 0xc0000b61f8 &assertion_test.Member{Name:"Eri", Grade:3} Test: TestSame Messages: not same types: *assertion_test.Member, assertion_test.Student --- FAIL: TestSame (0.00s)
Exactly
- 2つのオブジェクトが同一かどうかをテストする
- TODO: すみません
Equal
との違いが不明です
特定の条件を満たす値かどうか
Nil(NotNil)
Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
- オブジェクトが
nil
であるかをテストする
Empty(NotEmpty)
Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool
- オブジェクトの内容が空であることをテストする
- 次項の「
Empty
とZero
の違い」も参照
Zero(NotZero)
Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool
- 値がその型のzero valueであることをテストする
Empty
と Zero
の違い
Zero
は、値そのものが zero value であることをチェックする- array, slice, map, channel, ポインタの場合
- 値が
nil
かどうか
- 値が
- array, slice, map, channel, ポインタの場合
Empty
は、その値が指し示すものが「空」とみなせるかどうかをチェックする- array, slice, map, channel の場合
- 長さが 0 であるかどうか
- ポインタの場合
- それが指し占めす値が空であれば、空とみなされる
- array, slice, map, channel の場合
func TestEmptyZero(t *testing.T) { i := 0 ip := &i assert.Empty(t, i) assert.Empty(t, ip) // ip が指し示す i は zero value assert.Zero(t, i) assert.NotZero(t, ip) // ip 自体は値を持つ var se := []string{} // []string{} var sn []string // []string(nil) assert.Empty(t, se) assert.Empty(t, sn) assert.NotZero(t, se) // 空の slice を持つ assert.Zero(t, sn) // slice 自体がない }
True/False
True(t TestingT, value bool, msgAndArgs ...interface{}) bool
- オブジェクトの値が
true
またはfalse
と判定されるかどうかをテストする
Len
Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool
- オブジェクトの長さが期待通りであることをテストする
- built-in 関数の
len()
を実行可能なオブジェクトに対してのみ適用できる- 適用できない場合はテストに失敗する
func TestLen(t *testing.T) { assert.Len(t, "Rin", 3) }
Regexp(NotRegexp)
Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool
- 実際に得られた文字列が、指定の正規表現に合致することをテストする
- 期待される正規表現(
rx
)は、*regexp.Regexp
型、もしくはstring
で表現された正規表現のどちらでもよい
func TestRegexp(t *testing.T) { str := "nikko nikko nii" assert.Regexp(t, regexp.MustCompile("^(nikko )+nii$"), str) assert.Regexp(t, "^(nikko )+nii$", str) assert.NotRegexp(t, "^(nikko ){3}nii$", str) }
リスト
Contains/NotContains
Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool
- オブジェクトが特定の値を含むことをテストする
- 適用可能なのは、
string
、リスト(array, slice 等len()
の適用が可能なもの)、map
に限られる- それ以外のオブジェクトに適用した場合はテストに失敗する
string
の場合、部分文字列として含まれるかどうかをテストする- リストの場合、特定の要素がリスト内に存在するかどうかをテストする
map
の場合、特定の key が map 内に含まれるかどうかをテストする- map の value は比較の対象外
func TestContains(t *testing.T) { assert.Contains(t, "Maki Nishikino", "Maki") assert.Contains(t, []string{"Maki", "Hanayo", "Rin"}, "Maki") assert.Contains(t, map[string]int{"Maki": 1, "Honoka": 2, "Niko": 3}, "Maki") // map の value は対象外 assert.NotContains(t, map[string]int{"Maki": 1, "Honoka": 2, "Niko": 3}, 3) }
Subset(NotSubset)
Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool)
- リスト型のオブジェクトに対して、期待されたサブセットが含まれるかどうかをテストする
func TestSubset(t *testing.T) { members := []string{"Kotori", "Kayo", "Eri"} assert.Subset(t, members, []string{"Kotori", "Eri"}) assert.NotSubset(t, members, []string{"Kotori", "Yukiho"}) }
ElementsMatch
ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool)
- リスト型のオブジェクトに対して、すべての要素が含まれていることをテストする
- 要素の順番は問わない。ただし個数は一致する必要がある
func TestElementsMatch(t *testing.T) { members := []string{"Kotori", "Kayo", "Eri", "Kayo"} assert.ElementsMatch(t, members, []string{"Eri", "Kayo", "Kayo", "Kotori"}) //assert.ElementsMatch(t, members, []string{"Eri", "Kotori", "Kayo", "Kotori"}) //fail }
誤差の判定
WithinDuration
WithinDuration( t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool
time.Time
で表される時刻と、期待する時刻との誤差が一定以下であることをテストする
func TestWithinDuration(t *testing.T) { now := time.Now() expected := now.Add(2 * time.Second) assert.WithinDuration(t, expected, now, time.Duration(5*time.Second)) //assert.WithinDuration(t, expected, now, time.Duration(1*time.Second)) // fail }
InDelta
InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool
- 両者の誤差が一定値(
delta
)以下であることをテストする。
func TestInDelta(t *testing.T) { val := 10.0 / 2.998 assert.InDelta(t, 10.0, val*2.99, 0.1) }
InDeltaSlice
InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool
InDelta
のスライス版- 2つのスライスに含まれる同一位置の数値同士を比較し、その誤差が一定値(
delta
)以下であることをテストする - テストされる側(
actual
)のすべての要素に対してテストが通った場合に、InDeltaSlice
のテストも通る
func TestInDeltaSlice(t *testing.T) { expect := []float32{1.0, 2.0, 3.0} actual := []float32{1.03, 1.98, 3.01} assert.InDeltaSlice(t, expect, actual, 0.03) }
InDeltaMapValues
InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool
- 比較対象は map 型
- すべての key に対応する値を期待値、実際値で比較し、その誤差が範囲内に収まっていることをテストする
- key が完全に一致しない場合は FAIL となる
func TestInDeltaMapValues(t *testing.T) { expected := map[string]float64{"width": 2.5, "height": 7.5} actual := map[string]float64{"width": 2.47, "height": 7.53} assert.InDeltaMapValues(t, expected, actual, 0.1) }
InEpsilon
InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool
- 期待値対する実測値の誤差が一定割合以内に収まっていることをテストする
InDelta
と似ているが、InEpsilon
では誤差の「割合」をチェックする
func TestInEpsilon(t *testing.T) { assert.InEpsilon(t, 10000, 10090, 0.01) assert.InEpsilon(t, 100, 100.9, 0.01) // assert.InEpsilon(t, 1, 0.11, 0.01) <-- fail }
InEpsilonSlice
InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool
InEpsilon
のスライス版
ファイル
FileExists(NoFileExists)
FileExists(t TestingT, path string, msgAndArgs ...interface{}) bool
- 指定したファイルが存在することをテストする
- 指定したパスがディレクトリである場合はテスト失敗となる
func TestFileExists(t *testing.T) { // exists.txt はすでに存在し、missing.txt はないものとする assert.FileExists(t, "./exists.txt") assert.NoFileExists(t, "./missing.txt") // existsdir は存在するディレクトリ assert.NoFileExists(t, "./existsdir") }
DirExists(NoDirExists)
func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool {
- 指定したディレクトリが存在することをテストする
- 指定したパスがファイルであった場合はテストに失敗する
func TestDirExists(t *testing.T) { // existsdir は存在するディレクトリ、missingdir はないものとする assert.DirExists(t, "./existsdir") assert.NoDirExists(t, "./missingdir") // exists.txt はファイル assert.NoDirExists(t, "./exists.txt") }
JSON, YAML
JSONEq
JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool
- 実際値のJSON文字列が、期待されるJSON文字列と内容的に同一であることをテストする
- 文字列内での順番が異なってもかまわない
func TestJSONEq(t *testing.T) { expected := `{"name": "Nozomi", "age": 17, "blood": "O"}` actual := `{"age": 17, "blood": "O", "name": "Nozomi"}` assert.JSONEq(t, expected, actual) }
YAMLEq
YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool
- 実際値のYAML文字列が、期待されるYAML文字列と内容的に同一であることをテストする
- 文字列内での順番が異なってもかまわない
func TestYAMLEq(t *testing.T) { expected := `--- name: Eri age: 17 blood: B` actual := `--- blood: B age: 17 name: Eri` assert.YAMLEq(t, expected, actual) }
状態変化
Eventually
Eventually( t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool
- 一定時間内(
waitFor
)に条件が満たされた状態になることをテストする - 条件が満たされているかをチェックするための関数(
condition
)を引数で渡す- 条件が満たされたら
true
、そうでなければfalse
を返す - この関数は
tick
で指定した時間おきに呼び出される
- 条件が満たされたら
func TestEventually(t *testing.T) { var content []byte go func() { res, err := http.Get("https://example.com") if err != nil { return } defer res.Body.Close() content, _ = io.ReadAll(res.Body) }() // コンテンツが読み込まれたかをチェックする関数 checkCompleted := func() bool { if len(content) > 0 { return true } return false } assert.Eventually(t, checkCompleted, time.Second*5, time.Second*1) }
Never
Never( t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool
Eventually
の逆。一定時間内に条件が満たされないことをテストする。
エラーおよび Panic
Error(NoError)
Error(t TestingT, err error, msgAndArgs ...interface{}) bool
- error が存在する(
nil
でない)ことをテストする
func TestError(t *testing.T) { divideFunc := func(a, b int) (int, error) { if b == 0 { return 0, fmt.Errorf("division by zero") } return a / b, nil } _, err := divideFunc(5, 0) assert.Error(t, err) _, err = divideFunc(5, 2) assert.NoError(t, err) }
EqualError
EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool
- 得られたエラーが、期待される文字列表現と合致するかどうかをテストする
- テスト対象(
theError
)のエラーの文字列表現がerrString
と完全一致していればよい - 他の
assert
関数と引数の順番が異なるので注意実際に得られたエラー
期待されるエラー文字列
の順
var firstGrades map[string]string = map[string]string{ "Hanayo": "Koizumi", "Rin": "Hoshizora", "Maki": "Nishikino", } type ErrorNotFirstGrade struct{} func (e *ErrorNotFirstGrade) Error() string { return "not a first grade member" } func getLastName(firstName string) (string, error) { ln, ok := firstGrades[firstName] if !ok { return "", new(ErrorNotFirstGrade) } return ln, nil } func TestEqualError(t *testing.T) { _, err := getLastName("Honoka") assert.EqualError(t, err, "not a first grade member") }
ErrorContains
ErrorContains(t TestingT, theError error, contains string, msgAndArgs ...interface{}) bool
- 得られたエラーが、期待される文字列を含んでいるかどうかをテストする
- テスト対象(
theError
)のエラーの文字列表現がerrString
を含んでいればよい EqualError
と同様、他のassert
関数と引数の順番が異なるので注意実際に得られたエラー
期待される部分エラー文字列
の順
func TestErrorContains(t *testing.T) { err := errors.New("Honoka and Hanayo were captured by Umi in front of a rice restaurant") assert.ErrorContains(t, err, "captured") }
ErrorIs(NotErrorIs)
ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool
- 対象エラーのエラーチェーンの中に、特定のエラーが含まれていることをテストする
- エラーチェーンの探索には
errors.Is()
(https://golang.org/pkg/errors/#Is) が使用される
func TestErrorIs(t *testing.T) { errA := errors.New("Umi cannot write lyrics") errB := fmt.Errorf("Umi has escaped: %w", errA) errC := fmt.Errorf("Umi not found: %w", errB) assert.ErrorIs(t, errC, errA) }
ErrorAs
ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{}) bool
- 対象エラーのエラーチェーンの中に、
target
に代入可能なエラーが含まれていることをテストする - テストが通った場合、
target
にそのエラーが格納される - エラーチェーンの探索および
target
への代入は、errors.As()
(https://golang.org/pkg/errors/#As) の挙動と同じ
type KotoriSlumpError struct{} func (e *KotoriSlumpError) Error() string { return "Kotori cannot design dresses" } func TestErrorAs(t *testing.T) { errA := &KotoriSlumpError{} errB := fmt.Errorf("Kotori has escaped: %w", errA) errC := fmt.Errorf("Kotori not found: %w", errB) var err *KotoriSlumpError assert.ErrorAs(t, errC, &err) fmt.Println(err) // Kotori cannot design dresses }
Panics(NotPanics)
Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool
- 指定された関数が panic を起こすことをテストする
- 指定できる関数の型は
func()
(assert.PanicTestFunc
型) recover()
実行時に得られる panic value を確認するPanicsWithValue
PanicsWithError
も提供されている
func TestPanics(t *testing.T) { setQuantity := func(qty int) func() { return func() { if qty <= 0 { panic(fmt.Errorf("qty cannot be negative")) } } } assert.PanicsWithError(t, "qty cannot be negative", setQuantity(-2)) assert.NotPanics(t, setQuantity(5)) }
複雑な条件を使う
Condition
Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool
- テストの成否を複雑な条件で判定させたいときに使う
- 成否判定を行う
func() bool
型の関数を引数で渡す
type Member struct { Name string Grade int } func TestComparison(t *testing.T) { is3rdGrade := func(m Member) func() bool { return func() bool { return m.Grade == 3 } } assert.Condition(t, is3rdGrade(Member{"Niko", 3})) //assert.Condition(t, is3rdGrade(Member{"Honoka", 2})) // fail }
個人的見解
以上のように、assert
には多くのテスト関数が用意されている。しかし実際には Equal()
のみ使えば十分で、それ以外の関数は Equal()
と go 標準の文法の組み合わせだけで十分表現可能なケースがほとんどと思われる。
むしろ Equal()
以外の様々なテスト関数を駆使「しすぎる」ことによって、テストコードの意図が汲み取りづらくなってしまう恐れがある。また testify/assert
の学習コストを上げることにもなってしまう。特に複数メンバーの開発チーム内で使用する場合はこの点に注意すべきで、testify/assert
の便利さと、テストコードの共通理解の得られやすさのバランスをうまくとるべきだと思う。
(使うとしても True()
Nil()
など、誰が見てもほぼ誤解の余地がないようなものにとどめるのが良さそう)