Тестирование Лекция 5 Фёдор Короткий * go test - `*_test.go` файлы не являются частью пакета, а содержат тесты. - `go`test` создаёт нужный main, компилирует исполняемый файл и запускает его. * Тестовые функции Тестовые функции должны иметь сигнатуру: func TestName(t *testing.T) { // ... } Параметр `*testing.T` используется, чтобы сообщить о падении теста. * Пример Palindrome .play word1/word.go /func IsPa/,/^}/ .play word1/word_test.go /func TestPa/,/^}/ .play word1/word_test.go /func TestNonPa/,/^}/ * Пример Palindrome .play word1/word_test.go /func TestFrench/,/OMIT/ $ go test -v -run="French|Canal" === RUN TestFrenchPalindrome --- FAIL: TestFrenchPalindrome (0.00s) word_test.go:28: IsPalindrome("été") = false === RUN TestCanalPalindrome --- FAIL: TestCanalPalindrome (0.00s) word_test.go:35: IsPalindrome("A man, a plan, a canal: Panama") = false FAIL exit status 1 FAIL gopl.io/ch11/word1 0.014s * Пример Palindrome .play word2/word.go /func IsPa/,/^}/ * Table Driven Test .play word2/word_test.go /func Test/,/^}/ * Пример Echo .play echo/echo.go /var/,/OMIT/ * Пример Echo .play echo/echo_test.go /func TestEcho/,/^}/ * External Tests $ go list -f={{.GoFiles}} fmt [doc.go format.go print.go scan.go] $ go list -f={{.TestGoFiles}} fmt [export_test.go] $ go list -f={{.XTestGoFiles}} fmt [fmt_test.go scan_test.go stringer_test.go] Package test package fmt func TestXXX(t *testing.T) {} External test package fmt_test func TestYYY(t *testing.T) {} * fmt isSpace example // export_test.go package fmt var IsSpace = isSpace - fmt не зависит от unicode, и содержит упрощённую реализацию isSpace. - External тесты fmt проверяют, что fmt.isSpace и unicode.IsSpace не отличаются в поведении. * Writing Effective tests Bad example import ( "fmt" "strings" "testing" ) // A poor assertion function. func assertEqual(x, y int) { if x != y { panic(fmt.Sprintf("%d != %d", x, y)) } } func TestSplit(t *testing.T) { words := strings.Split("a:b:c", ":") assertEqual(len(words), 4) // ... } Тест упадёт с сообщением `3 != 4` после страниц стектрейсов. * Writing Effective tests Good example func TestSplit(t *testing.T) { s, sep := "a:b:c", ":" words := strings.Split(s, sep) if got, want := len(words), 3; got != want { t.Errorf("Split(%q, %q) returned %d words, want %d", s, sep, got, want) } // ... } * Завершение теста type Banana struct { Color string Tasty bool } func TestBanana(t *testing.T) { banana, err := GetBanana() if err != nil { t.Fatalf("GetBanana() failed: %v", err) } if banana.Color != "yellow" { t.Errorf("banana colors is %s, want yellow", banana.Color) } if !banana.Tasty { t.Errorf("banana is not tasty") } } * Завершение теста - Иногда тест нужно завершить преждевременно. t.Fatal("gcc not found in PATH") t.Fatalf("request failed: %v", err) t.FailNow() - Иногда тест нужно продолжать, чтобы узнать больше информации t.Error("i got a bad feeling about this") t.Errorf("%d documents found, want %d", 2, 3) t.Fail() - Иногда нужно просто залогировать информацию на будущее t.Logf("using go from %s", path) * Тестовые проверки в других горутинах func TestGo(t *testing.T) { var wg sync.WaitGroup wg.Add(2) go func () { defer wg.Done() // This is OK t.Error("1 != 2") }() go func () { defer wg.Done() // This is INVALID t.Fatal("1 != 2") }() wg.Wait() } * Горутины и завершение теста func TestGo(t *testing.T) { go func() { for { time.Sleep(time.Second) t.Logf("tick") // This will panic } }() } * Правильное завершение func TestGo(t *testing.T) { var wg sync.WaitGroup defer wg.Wait() ctx, cancel := context.WithCancel(context.Background()) defer cancel() wg.Add(1) go func() { defer wg.Done() for { select { case <-time.After(time.Second): case <-ctx.Done(): return } t.Logf("tick") } }() } * testify func TestSum(t *testing.T) { if got, want := Sum(1, 2), 4; got != want { t.Errorf("Sum(%d, %d) = %d, want %d", 1, 2, got, want) } } === RUN TestSum --- FAIL: TestSum (0.00s) example_test.go:11: Sum(1, 2) = 3, want 4 FAIL * testify import ( "testing" "github.com/stretchr/testify/assert" ) func TestSum0(t *testing.T) { assert.Equalf(t, 4, Sum(1, 2), "Sum(%d, %d)", 1, 2) } === RUN TestSum0 --- FAIL: TestSum0 (0.00s) example_test.go:20: Error Trace: example_test.go:20 Error: Not equal: expected: 4 actual : 3 Test: TestSum0 Messages: Sum(1, 2) - Функции из пакета `assert` работают как `t.Errorf`. - Функции из пакета `require` работают как `t.Fatalf`. * testify func TestCall(t *testing.T) { res, err := Call() require.NoError(t, err) assert.Equal(t, 42, res) } - Для проверок ошибок используйте `require.Error` и `require.NoError`. * Coverage .play size/size.go /func/,/^}/ * Coverage .play size/size_test.go /func/,/^}/ go test -cover PASS coverage: 42.9% of statements ok gitlab.com/manytask/itmo-go/public/lectures/04-testing/size 0.001s * Coverage go test -coverprofile=coverage.out go tool cover -html=coverage.out .image size/coverage.png * Benchmark Functions func BenchmarkIsPalindrome(b *testing.B) { for i := 0; i < b.N; i++ { IsPalindrome("A man, a plan, a canal: Panama") } } $ go test -bench=. PASS BenchmarkIsPalindrome-8 1000000 1035 ns/op ok gopl.io/ch11/word2 2.179s - `b.ReportAllocs()` включает подсчёт аллокаций - `-benchmem` включает подсчёт аллокаций глобально * Benchmark & Test Parameters func benchmark(b *testing.B, size int) { /* ... */ } func Benchmark10(b *testing.B) { benchmark(b, 10) } func Benchmark100(b *testing.B) { benchmark(b, 100) } func Benchmark1000(b *testing.B) { benchmark(b, 1000) } Или через под-тесты func benchmark(b *testing.B, size int) { /* ... */ } func BenchmarkN(b *testing.B) { for _, n := range []int{10, 100, 1000} { b.Run(fmt.Sprint(n), func(b *testing.B) { benchmark(b, n) }) } } * Fuzzing func Reverse(s string) string { b := []byte(s) for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 { b[i], b[j] = b[j], b[i] } return string(b) } * Fuzzing func FuzzReverse(f *testing.F) { testcases := []string{"Hello, world", " ", "!12345"} for _, tc := range testcases { f.Add(tc) // Use f.Add to provide a seed corpus } f.Fuzz(func(t *testing.T, orig string) { rev := Reverse(orig) doubleRev := Reverse(rev) if orig != doubleRev { t.Errorf("Before: %q, after: %q", orig, doubleRev) } if utf8.ValidString(orig) && !utf8.ValidString(rev) { t.Errorf("Reverse produced invalid UTF-8 string %q", rev) } }) } * Fuzzing go test -fuzz=Fuzz fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers fuzz: minimizing 38-byte failing input file... --- FAIL: FuzzReverse (0.01s) --- FAIL: FuzzReverse (0.00s) reverse_test.go:20: Reverse produced invalid UTF-8 string "\x9c\xdd" Failing input written to testdata/fuzz/FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a To re-run: go test -run=FuzzReverse/af69258a12129d6cbba438df5d5f25ba0ec050461c116f777e77ea7c9a0d217a FAIL exit status 1 FAIL example/fuzz 0.030s Что сломало реализацию? go test fuzz v1 string("泃") * Parallel tests func TestA(t *testing.T) { t.Parallel() // ... } func TestB(t *testing.T) { t.Parallel() // ... } func TestC(t *testing.T) { // ... } * Example Tests func ExampleIsPalindrome() { fmt.Println(IsPalindrome("A man, a plan, a canal: Panama")) fmt.Println(IsPalindrome("palindrome")) // Output: // true // false } Если `Output` нет, то `Example` служит только для документации. func ExampleAPI() { var c *Client // skip initialization rsp, err := c.Call(&Request{}) _ = err _ = rsp } * TestMain Иногда нужно сделать глобальную инициализацию. func TestMain(m *testing.M) { if os.Getenv("INSIDE_DOCKER") == "" { os.Exit(runSelfInDocker()) } os.Exit(m.Run()) } * t.Helper() func assertGood(t *testing.T, i int) { if i != 0 { t.Errorf("i (= %d) != 0", i) } } func TestA(t *testing.T) { // which one failed? assertGood(t, 0) assertGood(t, 1) } === RUN TestA --- FAIL: TestA (0.00s) example_test.go:25: i (= 1) != 0 FAIL * t.Helper() func assertGood(t *testing.T, i int) { t.Helper() if i != 0 { t.Errorf("i (= %d) != 0", i) } } func TestA(t *testing.T) { assertGood(t, 0) assertGood(t, 1) // line 32 } === RUN TestA --- FAIL: TestA (0.00s) example_test.go:32: i (1) != 0 FAIL * t.Skip() func TestingDB(t *testing.T) { dbConn := os.Getenv("DB") if dbConn == "off" { t.Skipf("DB=off is set; disabling tests relying on database") } } Иногда полезно пропускать тесты, которые используют внешние зависимости. * Test Fixtures type env struct { Client *s3.Client DB *sql.Conn } func newEnv(t *testing.T) (*env, func()) { // ... } func TestA(t *testing.T) { env, stop := newEnv(t) defer stop() // ... } func TestB(t *testing.T) { env, stop := newEnv(t) defer stop() // ... } * t.Cleanup() func newEnv(t *testing.T) *env { // ... t.Cleanup(func() { DB.Close() }) } func TestA(t *testing.T) { env := newEnv(t) // ... } * Fixture Composition type MyFixture struct { other.Fixture third.Fixture } func newFixture(t *testing.T) (*MyFixture, func()) { other, stopOther := other.NewFixture(t) third, stopThird := third.NewFixture(t) return &MyFixture{other, third}, func() { stopOther() stopThird() } } * Race detector .play race/race_test.go * Race detector prime@bee ~/C/shad-go> go test -race ./lectures/04-testing/race ================== WARNING: DATA RACE Read at 0x00c000092090 by goroutine 8: gitlab.com/manytask/itmo-go/public/lectures/04-testing/race.TestRace() /home/prime/Code/shad-go/lectures/04-testing/race/race_test.go:25 +0x144 testing.tRunner() /usr/local/go/src/testing/testing.go:909 +0x199 Previous write at 0x00c000092090 by goroutine 9: gitlab.com/manytask/itmo-go/public/lectures/04-testing/race.TestRace.func1() /home/prime/Code/shad-go/lectures/04-testing/race/race_test.go:17 +0x6c ... ================== --- FAIL: TestRace (0.00s) testing.go:853: race detected during execution of test FAIL FAIL gitlab.com/manytask/itmo-go/public/lectures/04-testing/race 0.007s * White box testing .play mocks/mocks.go /func CheckQuota/,/OMIT/ * White box testing .play mocks/mocks_test.go /func/,/^}/ * gomock .play gomock/example.go - Запуск `go`generate`.` создаст файл `mock.go` - Хорошая идея - класть `mock`-и в отдельный пакет. * gomock .play gomock/example_test.go * httptest .play httptest/main.go /func/,/^}/ * golden files func TestExample(t *testing.T) { recorder := httptest.NewRecorder() req, err := http.NewRequest("GET", "/example", nil) require.NoError(t, err) handler := http.HandlerFunc(ExampleHandler) handler.ServeHTTP(req, recorder) g := goldie.New(t) g.Assert(t, "example", recorder.Body.Bytes()) } - `go`test`-update` сохраняет результат в файл - `go`test` сравнивает вывод с сохранённым результатом