Тестирование
Лекция 5
Максим Иванов
Максим Иванов
*_test.go файлы не являются частью пакета, а содержат тесты.go test создаёт нужный main, компилирует исполняемый файл и запускает его.Тестовые функции должны иметь сигнатуру:
func TestName(t *testing.T) {
// ...
}
Параметр *testing.T используется, чтобы сообщить о падении теста.
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// See page 303.
//!+
// Package word provides utilities for word games.
package word
// IsPalindrome reports whether s reads the same forward and backward.
// (Our first attempt.)
func IsPalindrome(s string) bool { for i := range s { if s[i] != s[len(s)-1-i] { return false } } return true }
//!-
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// !+test
package word
import "testing"
func TestPalindrome(t *testing.T) { if !IsPalindrome("detartrated") { t.Error(`IsPalindrome("detartrated") = false`) } if !IsPalindrome("kayak") { t.Error(`IsPalindrome("kayak") = false`) } }
func TestNonPalindrome(t *testing.T) {
if IsPalindrome("palindrome") {
t.Error(`IsPalindrome("palindrome") = true`)
}
}
// The tests below are expected to fail.
// See package gopl.io/ch11/word2 for the fix.
func TestFrenchPalindrome(t *testing.T) {
if !IsPalindrome("été") {
t.Error(`IsPalindrome("été") = false`)
}
}
func TestCanalPalindrome(t *testing.T) {
input := "A man, a plan, a canal: Panama"
if !IsPalindrome(input) {
t.Errorf(`IsPalindrome(%q) = false`, input)
}
}
// OMIT
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// !+test
package word
import "testing"
func TestPalindrome(t *testing.T) {
if !IsPalindrome("detartrated") {
t.Error(`IsPalindrome("detartrated") = false`)
}
if !IsPalindrome("kayak") {
t.Error(`IsPalindrome("kayak") = false`)
}
}
func TestNonPalindrome(t *testing.T) { if IsPalindrome("palindrome") { t.Error(`IsPalindrome("palindrome") = true`) } }
// The tests below are expected to fail.
// See package gopl.io/ch11/word2 for the fix.
func TestFrenchPalindrome(t *testing.T) {
if !IsPalindrome("été") {
t.Error(`IsPalindrome("été") = false`)
}
}
func TestCanalPalindrome(t *testing.T) {
input := "A man, a plan, a canal: Panama"
if !IsPalindrome(input) {
t.Errorf(`IsPalindrome(%q) = false`, input)
}
}
// OMIT
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// !+test
package word
import "testing"
func TestPalindrome(t *testing.T) {
if !IsPalindrome("detartrated") {
t.Error(`IsPalindrome("detartrated") = false`)
}
if !IsPalindrome("kayak") {
t.Error(`IsPalindrome("kayak") = false`)
}
}
func TestNonPalindrome(t *testing.T) {
if IsPalindrome("palindrome") {
t.Error(`IsPalindrome("palindrome") = true`)
}
}
// The tests below are expected to fail.
// See package gopl.io/ch11/word2 for the fix.
func TestFrenchPalindrome(t *testing.T) { if !IsPalindrome("été") { t.Error(`IsPalindrome("été") = false`) } } func TestCanalPalindrome(t *testing.T) { input := "A man, a plan, a canal: Panama" if !IsPalindrome(input) { t.Errorf(`IsPalindrome(%q) = false`, input) } }
$ 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// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// See page 305.
//!+
// Package word provides utilities for word games.
package word
import "unicode"
// IsPalindrome reports whether s reads the same forward and backward.
// Letter case is ignored, as are non-letters.
func IsPalindrome(s string) bool { var letters []rune for _, r := range s { if unicode.IsLetter(r) { letters = append(letters, unicode.ToLower(r)) } } for i := range letters { if letters[i] != letters[len(letters)-1-i] { return false } } return true }
//!-
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
package word
import (
"fmt"
"math/rand"
"testing"
"time"
)
//!+bench
//!-bench
// !+test
func TestIsPalindrome(t *testing.T) { tests := []struct { input string want bool }{ {"", true}, {"a", true}, {"aa", true}, {"ab", false}, {"kayak", true}, {"detartrated", true}, {"A man, a plan, a canal: Panama", true}, {"Able was I ere I saw Elba", true}, {"été", true}, {"Et se resservir, ivresse reste.", true}, {"palindrome", false}, // non-palindrome {"desserts", false}, // semi-palindrome } for _, test := range tests { if got := IsPalindrome(test.input); got != test.want { t.Errorf("IsPalindrome(%q) = %v", test.input, got) } } }
//!-test
// !+bench
func BenchmarkIsPalindrome(b *testing.B) {
for i := 0; i < b.N; i++ {
IsPalindrome("A man, a plan, a canal: Panama")
}
}
//!-bench
//!+example
func ExampleIsPalindrome() {
fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
fmt.Println(IsPalindrome("palindrome"))
// Output:
// true
// false
}
//!-example
/*
//!+random
import "math/rand"
//!-random
*/
// !+random
// randomPalindrome returns a palindrome whose length and contents
// are derived from the pseudo-random number generator rng.
func randomPalindrome(rng *rand.Rand) string {
n := rng.Intn(25) // random length up to 24
runes := make([]rune, n)
for i := 0; i < (n+1)/2; i++ {
r := rune(rng.Intn(0x1000)) // random rune up to '\u0999'
runes[i] = r
runes[n-1-i] = r
}
return string(runes)
}
func TestRandomPalindromes(t *testing.T) {
// Initialize a pseudo-random number generator.
seed := time.Now().UTC().UnixNano()
t.Logf("Random seed: %d", seed)
rng := rand.New(rand.NewSource(seed))
for i := 0; i < 1000; i++ {
p := randomPalindrome(rng)
if !IsPalindrome(p) {
t.Errorf("IsPalindrome(%q) = false", p)
}
}
}
//!-random
/*
// Answer for Exercicse 11.1: Modify randomPalindrome to exercise
// IsPalindrome's handling of punctuation and spaces.
// WARNING: the conversion r -> upper -> lower doesn't preserve
// the value of r in some cases, e.g., µ Μ, ſ S, ı I
// randomPalindrome returns a palindrome whose length and contents
// are derived from the pseudo-random number generator rng.
func randomNoisyPalindrome(rng *rand.Rand) string {
n := rng.Intn(25) // random length up to 24
runes := make([]rune, n)
for i := 0; i < (n+1)/2; i++ {
r := rune(rng.Intn(0x200)) // random rune up to \u99
runes[i] = r
r1 := r
if unicode.IsLetter(r) && unicode.IsLower(r) {
r = unicode.ToUpper(r)
if unicode.ToLower(r) != r1 {
fmt.Printf("cap? %c %c\n", r1, r)
}
}
runes[n-1-i] = r
}
return "?" + string(runes) + "!"
}
func TestRandomNoisyPalindromes(t *testing.T) {
// Initialize a pseudo-random number generator.
seed := time.Now().UTC().UnixNano()
t.Logf("Random seed: %d", seed)
rng := rand.New(rand.NewSource(seed))
n := 0
for i := 0; i < 1000; i++ {
p := randomNoisyPalindrome(rng)
if !IsPalindrome(p) {
t.Errorf("IsNoisyPalindrome(%q) = false", p)
n++
}
}
fmt.Fprintf(os.Stderr, "fail = %d\n", n)
}
*/
$ 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) {}
`
// export_test.go
package fmt
var IsSpace = isSpace
отличаются в поведении.
9Bad 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` после страниц стектрейсов.
10Good 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")
}
}()
}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
FAILimport (
"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.func TestCall(t *testing.T) {
res, err := Call()
require.NoError(t, err)
assert.Equal(t, 42, res)
}require.Error и require.NoError.package size
func Size(a int) string { switch { case a < 0: return "negative" case a == 0: return "zero" case a < 10: return "small" case a < 100: return "big" case a < 1000: return "huge" } return "enormous" }
package size
import "testing"
func TestSize(t *testing.T) { type Test struct { in int out string } tests := []Test{ {-1, "negative"}, {5, "small"}, } for i, test := range tests { size := Size(test.in) if size != test.out { t.Errorf("#%d: Size(%d)=%s; want %s", i, test.in, size, test.out) } } }
go test -cover PASS coverage: 42.9% of statements ok gitlab.com/slon/shad-go/lectures/04-testing/size 0.001s
go test -coverprofile=coverage.out go tool cover -html=coverage.out
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$ go test -bench=. -run=^$ $ go test -bench='^BenchmarkIsPalindrome$' -run=^$ $ go test ./... -bench=. -run=^$
-run=^$ отключает запуск обычных тестов, оставляя только бенчмарки.// Единовременная подготовка до измерений
func BenchmarkDoWork(b *testing.B) {
data := makeBigData()
b.ResetTimer() // сброс таймера перед измерением основного тела
for i := 0; i < b.N; i++ {
_ = DoWork(data)
}
}
// Подготовка на каждую итерацию вне таймера
func BenchmarkDoWorkPerIter(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer()
in := prepareInput(i)
b.StartTimer()
_ = DoWork(in)
}
}b.ResetTimer() для единоразовой подготовки и b.StopTimer()/b.StartTimer() для пер‑итерационной подготовки, чтобы не учитывать её во времени бенчмарка.func BenchmarkAlloc(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = bytes.Repeat([]byte("x"), 10)
}
}
$ go test -bench=. -benchmem -run=^$b.ReportAllocs() включает отчёт по аллокациям в конкретном бенчмарке.-benchmem к `go test`.b.N подбирается автоматически, чтобы прогон занял ~1 секунду.-benchtime:$ go test -bench=. -benchtime=3s -run=^$ $ go test -bench=BenchmarkDoWork -benchtime=100x -run=^$
-cpu=1,2,4 (запустит бенчмарк для каждого GOMAXPROCS).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)
})
}
}
$ go test -bench='^BenchmarkSizes/100$' -run=^$
$ go test -bench='BenchmarkSizes/(100|1000)$' -run=^$-bench.benchstat (golang.org/x/perf/benchstat) агрегирует и сравнивает результаты бенчмарков.$ go test -bench=. -benchmem -run=^$ > old.txt $ go test -bench=. -benchmem -run=^$ > new.txt $ benchstat old.txt new.txt
benchstat.Collection в tools/testtool/commands/test_submission.go.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)
}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)
}
})
}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("泃")func TestA(t *testing.T) {
t.Parallel()
// ...
}
func TestB(t *testing.T) {
t.Parallel()
// ...
}
func TestC(t *testing.T) {
// ...
}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
}Иногда нужно сделать глобальную инициализацию.
func TestMain(m *testing.M) {
if os.Getenv("INSIDE_DOCKER") == "" {
os.Exit(runSelfInDocker())
}
os.Exit(m.Run())
}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
FAILfunc 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
FAILfunc TestingDB(t *testing.T) {
dbConn := os.Getenv("DB")
if dbConn == "off" {
t.Skipf("DB=off is set; disabling tests relying on database")
}
}Иногда полезно пропускать тесты, которые используют внешние зависимости.
38type 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()
// ...
}func newEnv(t *testing.T) *env {
// ...
t.Cleanup(func() {
DB.Close()
})
}
func TestA(t *testing.T) {
env := newEnv(t)
// ...
}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()
}
}package race import ( "sync" "testing" ) func TestRace(t *testing.T) { var wg sync.WaitGroup defer wg.Wait() wg.Add(2) var i int go func() { defer wg.Done() i = 0 }() go func() { defer wg.Done() i = 1 }() _ = i }
prime@bee ~/C/shad-go> go test -race ./lectures/04-testing/race
==================
WARNING: DATA RACE
Read at 0x00c000092090 by goroutine 8:
gitlab.com/slon/shad-go/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/slon/shad-go/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/slon/shad-go/lectures/04-testing/race 0.007s// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// Echo prints its command-line arguments.
package main
import (
"flag"
"fmt"
"io"
"os"
"strings"
)
var ( n = flag.Bool("n", false, "omit trailing newline") s = flag.String("s", " ", "separator") ) var out io.Writer = os.Stdout // modified during testing func main() { flag.Parse() if err := echo(!*n, *s, flag.Args()); err != nil { fmt.Fprintf(os.Stderr, "echo: %v\n", err) os.Exit(1) } } func echo(newline bool, sep string, args []string) error { fmt.Fprint(out, strings.Join(args, sep)) if newline { fmt.Fprintln(out) } return nil }
// Copyright © 2016 Alan A. A. Donovan & Brian W. Kernighan.
// License: https://creativecommons.org/licenses/by-nc-sa/4.0/
// Test of echo command. Run with: go test gopl.io/ch11/echo
// !+
package main
import (
"bytes"
"fmt"
"testing"
)
func TestEcho(t *testing.T) { tests := []struct { newline bool sep string args []string want string }{ {true, "", []string{}, "\n"}, {false, "", []string{}, ""}, {true, "\t", []string{"one", "two", "three"}, "one\ttwo\tthree\n"}, {true, ",", []string{"a", "b", "c"}, "a,b,c\n"}, } for _, test := range tests { descr := fmt.Sprintf("echo(%v, %q, %q)", test.newline, test.sep, test.args) out = new(bytes.Buffer) // captured output if err := echo(test.newline, test.sep, test.args); err != nil { t.Errorf("%s failed: %v", descr, err) continue } got := out.(*bytes.Buffer).String() if got != test.want { t.Errorf("%s = %q, want %q", descr, got, test.want) } } }
package mocks
import (
"fmt"
"log"
"net/smtp"
)
var (
sender, password, hostname string
template = "%d. %d%% of your quota"
)
func bytesInUse(username string) int {
return 10000000000
}
func CheckQuota(username string) { used := bytesInUse(username) const quota = 1000000000 // 1GB percent := 100 * used / quota if percent < 90 { return // OK } msg := fmt.Sprintf(template, used, percent) notifyUser(username, msg) } var notifyUser = doNotifyUser func doNotifyUser(username, msg string) { auth := smtp.PlainAuth("", sender, password, hostname) err := smtp.SendMail(hostname+":587", auth, sender, []string{username}, []byte(msg)) if err != nil { log.Printf("smtp.SendEmail(%s) failed: %s", username, err) } }
package mocks
import (
"strings"
"testing"
)
func TestCheckQuotaNotifiesUser(t *testing.T) { var notifiedUser, notifiedMsg string notifyUser = func(user, msg string) { notifiedUser, notifiedMsg = user, msg } // ...simulate a 980MB-used condition... const user = "joe@example.org" CheckQuota(user) if notifiedUser == "" && notifiedMsg == "" { t.Fatalf("notifyUser not called") } if notifiedUser != user { t.Errorf("wrong user (%s) notified, want %s", notifiedUser, user) } const wantSubstring = "98% of your quota" if !strings.Contains(notifiedMsg, wantSubstring) { t.Errorf("unexpected notification message <<%s>>, "+ "want substring %q", notifiedMsg, wantSubstring) } }
package example //go:generate mockgen -package example -destination mock.go . Foo type Foo interface { Bar(x int) int } func SUT(f Foo) { // ... }
go generate . создаст файл mock.gomock-и в отдельный пакет.package example import ( "testing" "github.com/golang/mock/gomock" ) func TestFoo(t *testing.T) { ctrl := gomock.NewController(t) m := NewMockFoo(ctrl) // Asserts that the first and only call to Bar() is passed 99. // Anything else will fail. m. EXPECT(). Bar(gomock.Eq(99)). Return(101) SUT(m) }
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)
func main() { handler := func(w http.ResponseWriter, r *http.Request) { _, _ = io.WriteString(w, "<html><body>Hello World!</body></html>") } req := httptest.NewRequest("GET", "http://example.com/foo", nil) w := httptest.NewRecorder() handler(w, req) resp := w.Result() body, _ := io.ReadAll(resp.Body) fmt.Println(resp.StatusCode) fmt.Println(resp.Header.Get("Content-Type")) fmt.Println(string(body)) }
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 сравнивает вывод с сохранённым результатомМаксим Иванов