注: 本記事は元々 Qiita にて公開していたものの再掲です。
概要
- testify
- golang でテストをより記述しやすくするためのパッケージ
- その中で
assert
パッケージは、結果値の妥当性をチェックする関数を提供する
本記事で使用したバージョン
テストの書き方と結果出力
import
import (
"github.com/stretchr/testify/assert"
)
引数の指定
(例) Equal
関数
assert.Equal(t, expected, got, description)
- 1番目の引数は、
testing.T
- そのあと、テスト関数ごとに指定の引数を追加する
- 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
- 実際値が期待値と一致していることをテストする
- 値はスカラー値だけでなく、構造体のインスタンス等でも比較可能
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)
}
- 一番下のテストは、ポインタは同一(
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
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, ポインタの場合
Empty
は、その値が指し示すものが「空」とみなせるかどうかをチェックする
- array, slice, map, channel の場合
- ポインタの場合
func TestEmptyZero(t *testing.T) {
i := 0
ip := &i
assert.Empty(t, i)
assert.Empty(t, ip)
assert.Zero(t, i)
assert.NotZero(t, ip)
var se := []string{}
var sn []string
assert.Empty(t, se)
assert.Empty(t, sn)
assert.NotZero(t, se)
assert.Zero(t, sn)
}
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 内に含まれるかどうかをテストする
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")
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"})
}
誤差の判定
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))
}
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)
}
InEpsilonSlice
InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool
ファイル
FileExists(NoFileExists)
FileExists(t TestingT, path string, msgAndArgs ...interface{}) bool
- 指定したファイルが存在することをテストする
- 指定したパスがディレクトリである場合はテスト失敗となる
func TestFileExists(t *testing.T) {
assert.FileExists(t, "./exists.txt")
assert.NoFileExists(t, "./missing.txt")
assert.NoFileExists(t, "./existsdir")
}
DirExists(NoDirExists)
func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool {
- 指定したディレクトリが存在することをテストする
- 指定したパスがファイルであった場合はテストに失敗する
func TestDirExists(t *testing.T) {
assert.DirExists(t, "./existsdir")
assert.NoDirExists(t, "./missingdir")
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
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
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)
}
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
には多くのテスト関数が用意されている。しかし実際には Equal()
のみ使えば十分で、それ以外の関数は Equal()
と go 標準の文法の組み合わせだけで十分表現可能なケースがほとんどと思われる。
むしろ Equal()
以外の様々なテスト関数を駆使「しすぎる」ことによって、テストコードの意図が汲み取りづらくなってしまう恐れがある。また testify/assert
の学習コストを上げることにもなってしまう。特に複数メンバーの開発チーム内で使用する場合はこの点に注意すべきで、testify/assert
の便利さと、テストコードの共通理解の得られやすさのバランスをうまくとるべきだと思う。
(使うとしても True()
Nil()
など、誰が見てもほぼ誤解の余地がないようなものにとどめるのが良さそう)