Slide 1

Slide 1 text

Тестирование в Go Go 1.10 Release Party Алексей Махов Avito

Slide 2

Slide 2 text

О чем поговорим stdlib go test practice & tips & tricks open source

Slide 3

Slide 3 text

test *_test.go

Slide 4

Slide 4 text

test *_test.go import ( "testing" )

Slide 5

Slide 5 text

test *_test.go import ( "testing" ) func TestAnything(t *testing.T) { }

Slide 6

Slide 6 text

testing

Slide 7

Slide 7 text

testing import ( "testing" ) *testing.T *testing.B

Slide 8

Slide 8 text

testing // TB is the interface common to T and B. type TB interface { Error(args ...interface{}) Errorf(format string, args ...interface{}) Fail() FailNow() Failed() bool Fatal(args ...interface{}) Fatalf(format string, args ...interface{}) Log(args ...interface{}) Logf(format string, args ...interface{}) Name() string Skip(args ...interface{}) SkipNow() Skipf(format string, args ...interface{}) Skipped() bool // A private method to prevent users implementing the // interface and so future additions to it will not // violate Go 1 compatibility. private() }

Slide 9

Slide 9 text

testing // TB is the interface common to T and B. type TB interface { Error(args ...interface{}) Errorf(format string, args ...interface{}) Fail() FailNow() Failed() bool Fatal(args ...interface{}) Fatalf(format string, args ...interface{}) Log(args ...interface{}) Logf(format string, args ...interface{}) Name() string Skip(args ...interface{}) SkipNow() Skipf(format string, args ...interface{}) Skipped() bool // A private method to prevent users implementing the // interface and so future additions to it will not // violate Go 1 compatibility. private() }

Slide 10

Slide 10 text

*testing.T ⚠ // TB is the interface common to T and B. type TB interface { Error(args ...interface{}) Errorf(format string, args ...interface{}) Fail() FailNow() Failed() bool Fatal(args ...interface{}) Fatalf(format string, args ...interface{}) Log(args ...interface{}) Logf(format string, args ...interface{}) Name() string Skip(args ...interface{}) SkipNow() Skipf(format string, args ...interface{}) Skipped() bool // A private method to prevent users implementing the // interface and so future additions to it will not // violate Go 1 compatibility. private() }

Slide 11

Slide 11 text

*testing.T // TB is the interface common to T and B. type TB interface { Error(args ...interface{}) Errorf(format string, args ...interface{}) Fail() FailNow() Failed() bool Fatal(args ...interface{}) Fatalf(format string, args ...interface{}) Log(args ...interface{}) Logf(format string, args ...interface{}) Name() string Skip(args ...interface{}) SkipNow() Skipf(format string, args ...interface{}) Skipped() bool // A private method to prevent users implementing the // interface and so future additions to it will not // violate Go 1 compatibility. private() }

Slide 12

Slide 12 text

*testing.T ❌ // TB is the interface common to T and B. type TB interface { Error(args ...interface{}) Errorf(format string, args ...interface{}) Fail() FailNow() Failed() bool Fatal(args ...interface{}) Fatalf(format string, args ...interface{}) Log(args ...interface{}) Logf(format string, args ...interface{}) Name() string Skip(args ...interface{}) SkipNow() Skipf(format string, args ...interface{}) Skipped() bool // A private method to prevent users implementing the // interface and so future additions to it will not // violate Go 1 compatibility. private() }

Slide 13

Slide 13 text

*testing.T t.Run(name string, f func (*testing.T)) bool t.Parallel()

Slide 14

Slide 14 text

Табличные тесты

Slide 15

Slide 15 text

Табличные тесты func TestTime(t *testing.T) { testCases := []struct { gmt string loc string want string }{ {"12:31", "Europe/Zuri", "13:31"}, {"12:31", "America/New_York", "7:31"}, {"08:08", "Australia/Sydney", "18:08"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("%s in %s", tc.gmt, tc.loc), func(t *testing.T) { loc, err := time.LoadLocation(tc.loc) if err != nil { t.Fatal("could not load location") } gmt, _ := time.Parse("15:04", tc.gmt) if got := gmt.In(loc).Format("15:04"); got != tc.want { t.Errorf("got %s; want %s", got, tc.want) } }) } }

Slide 16

Slide 16 text

Табличные тесты func TestTime(t *testing.T) { testCases := []struct { gmt string loc string want string }{ {"12:31", "Europe/Zuri", "13:31"}, {"12:31", "America/New_York", "7:31"}, {"08:08", "Australia/Sydney", "18:08"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("%s in %s", tc.gmt, tc.loc), func(t *testing.T) { loc, err := time.LoadLocation(tc.loc) if err != nil { t.Fatal("could not load location") } gmt, _ := time.Parse("15:04", tc.gmt) if got := gmt.In(loc).Format("15:04"); got != tc.want { t.Errorf("got %s; want %s", got, tc.want) } }) } }

Slide 17

Slide 17 text

Табличные тесты func TestTime(t *testing.T) { testCases := []struct { gmt string loc string want string }{ {"12:31", "Europe/Zuri", "13:31"}, {"12:31", "America/New_York", "7:31"}, {"08:08", "Australia/Sydney", "18:08"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("%s in %s", tc.gmt, tc.loc), func(t *testing.T) { loc, err := time.LoadLocation(tc.loc) if err != nil { t.Fatal("could not load location") } gmt, _ := time.Parse("15:04", tc.gmt) if got := gmt.In(loc).Format("15:04"); got != tc.want { t.Errorf("got %s; want %s", got, tc.want) } }) } }

Slide 18

Slide 18 text

Табличные тесты ❌ func TestTime(t *testing.T) { testCases := []struct { gmt string loc string want string }{ {"12:31", "Europe/Zuri", "13:31"}, {"12:31", "America/New_York", "7:31"}, {"08:08", "Australia/Sydney", "18:08"}, } for _, tc := range testCases { t.Run(fmt.Sprintf("%s in %s", tc.gmt, tc.loc), func(t *testing.T) { loc, err := time.LoadLocation(tc.loc) if err != nil { t.Fatal("could not load location") } gmt, _ := time.Parse("15:04", tc.gmt) if got := gmt.In(loc).Format("15:04"); got != tc.want { t.Errorf("got %s; want %s", got, tc.want) } }) } }

Slide 19

Slide 19 text

Табличные тесты ✅ func TestTime(t *testing.T) { testCases := map[string]struct { gmt string loc string want string }{ {"12:31 in Europe/Zuri": {"12:31", "Europe/Zuri", "13:31"}}, {"12:31 in America/New_York": {"12:31", "America/New_York", "7:31"}}, {"08:08 in Australia/Sydney": {"08:08", "Australia/Sydney", "18:08"}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { loc, err := time.LoadLocation(tc.loc) if err != nil { t.Fatal("could not load location") } gmt, _ := time.Parse("15:04", tc.gmt) if got := gmt.In(loc).Format("15:04"); got != tc.want { t.Errorf("got %s; want %s", got, tc.want) } }) } }

Slide 20

Slide 20 text

Табличные тесты ✅ func TestTime(t *testing.T) { testCases := []struct { gmt string loc string want string }{ {"12:31", "Europe/Zuri", "13:31"}, {"12:31", "America/New_York", "7:31"}, {"08:08", "Australia/Sydney", "18:08"}, } for name, tc := range testCases { t.Run("", func(t *testing.T) { loc, err := time.LoadLocation(tc.loc) if err != nil { t.Fatal("could not load location") } gmt, _ := time.Parse("15:04", tc.gmt) if got := gmt.In(loc).Format("15:04"); got != tc.want { t.Errorf("got %s; want %s", got, tc.want) } }) } }

Slide 21

Slide 21 text

Табличные тесты ✅ --- PASS: TestTime (0.00s) --- PASS: TestTime/#00 (0.00s) --- PASS: TestTime/#01 (0.00s) --- PASS: TestTime/#02 (0.00s)

Slide 22

Slide 22 text

Табличные тесты ✅ func TestTime(t *testing.T) { testCases := map[string]struct { gmt string loc string want string }{ {"12:31 in Europe/Zuri": {"12:31", "Europe/Zuri", "13:31"}}, {"12:31 in America/New_York": {"12:31", "America/New_York", "7:31"}}, {"08:08 in Australia/Sydney": {"08:08", "Australia/Sydney", "18:08"}}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { t.Parallel() loc, err := time.LoadLocation(tc.loc) if err != nil { t.Fatal("could not load location") } gmt, _ := time.Parse("15:04", tc.gmt) if got := gmt.In(loc).Format("15:04"); got != tc.want { t.Errorf("got %s; want %s", got, tc.want) } }) } }

Slide 23

Slide 23 text

testing/* testing/iotest testing/quick

Slide 24

Slide 24 text

net/http/httptest type ResponseRecorder type Server

Slide 25

Slide 25 text

httptest.ResponseRecorder func ExampleResponseRecorder() { handler := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "Hello World!") } req := httptest.NewRequest("GET", "http://example.com/foo", nil) w := httptest.NewRecorder() handler(w, req) resp := w.Result() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(resp.StatusCode) fmt.Println(resp.Header.Get("Content-Type")) fmt.Println(string(body)) // Output: // 200 // text/html; charset=utf-8 // Hello World! }

Slide 26

Slide 26 text

httptest.ResponseRecorder func ExampleResponseRecorder() { handler := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "Hello World!") } req := httptest.NewRequest("GET", "http://example.com/foo", nil) w := httptest.NewRecorder() handler(w, req) resp := w.Result() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(resp.StatusCode) fmt.Println(resp.Header.Get("Content-Type")) fmt.Println(string(body)) // Output: // 200 // text/html; charset=utf-8 // Hello World! }

Slide 27

Slide 27 text

httptest.ResponseRecorder func ExampleResponseRecorder() { handler := func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "Hello World!") } req := httptest.NewRequest("GET", "http://example.com/foo", nil) w := httptest.NewRecorder() handler(w, req) resp := w.Result() body, _ := ioutil.ReadAll(resp.Body) fmt.Println(resp.StatusCode) fmt.Println(resp.Header.Get("Content-Type")) fmt.Println(string(body)) // Output: // 200 // text/html; charset=utf-8 // Hello World! }

Slide 28

Slide 28 text

httptest.Server func ExampleServer() { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, client") })) defer ts.Close() res, err := http.Get(ts.URL) if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Printf("%s", greeting) // Output: Hello, client }

Slide 29

Slide 29 text

httptest.Server func ExampleServer() { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, client") })) defer ts.Close() res, err := http.Get(ts.URL) if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Printf("%s", greeting) // Output: Hello, client }

Slide 30

Slide 30 text

httptest.Server func ExampleServer() { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "Hello, client") })) defer ts.Close() res, err := http.Get(ts.URL) if err != nil { log.Fatal(err) } greeting, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { log.Fatal(err) } fmt.Printf("%s", greeting) // Output: Hello, client }

Slide 31

Slide 31 text

go test

Slide 32

Slide 32 text

go test ➜ go test ./pkg ok _/Users/amakhov/Documents/testing/testing_in_go/pkg 0.006s

Slide 33

Slide 33 text

go test -v ➜ go test -v ./pkg === RUN TestSum --- PASS: TestSum (0.00s) === RUN TestSum2 --- PASS: TestSum2 (0.00s) === RUN TestSum3 --- PASS: TestSum3 (0.00s) === RUN TestSum4 --- PASS: TestSum4 (0.00s) === RUN TestSum5 --- PASS: TestSum5 (0.00s) === RUN TestSum6 --- PASS: TestSum6 (0.00s) === RUN TestSum7 --- PASS: TestSum7 (0.00s) PASS ok _/Users/amakhov/Documents/testing/testing_in_go/pkg 0.006s

Slide 34

Slide 34 text

go test -list ➜ go test ./pkg -list .+ TestSum TestSum2 TestSum3 TestSum4 TestSum5 TestSum6 TestSum7 ok _/Users/amakhov/Documents/testing/testing_in_go/pkg 0.005s

Slide 35

Slide 35 text

go test -run go test -run=TestSum go test -run="TestSum[3-9]+"

Slide 36

Slide 36 text

go test -short if testing.Short() { t.Skip(…) }

Slide 37

Slide 37 text

go test -cover -covermode -coverpkg -coverpro le cover.out https://blog.golang.org/cover

Slide 38

Slide 38 text

go test -c ¯\_( ツ)_/¯

Slide 39

Slide 39 text

go test -timeout По умолчанию 10 минут

Slide 40

Slide 40 text

go 1.10 Кэширование результата go vet go test -failfast go test -json output

Slide 41

Slide 41 text

Фикстуры

Slide 42

Slide 42 text

Фикстуры ✅ func TestSomething(t *testing.T) { f, err := os.Open("testdata/somefixture.json") //... }

Slide 43

Slide 43 text

Golden les ✅ var update = flag.Bool("update", false, "update .golden files") func init() { flag.Parse() } func TestSomething(t *testing.T) { actual := doSomething() golden := filepath.Join("testdata", tc.Name+".golden") if *update { ioutil.WriteFile(golden, actual, 0644) } expected, _ := ioutil.ReadFile(golden) if !bytes.Equal(actual, expected) { // FAIL! } }

Slide 44

Slide 44 text

Golden les ✅ $ go test … $ go test -update …

Slide 45

Slide 45 text

test helpers ✅ func helperLoadFile(t *testing.T, name string) ([]byte, err) { path := filepath.Join("testdata", name) return ioutil.ReadFile(path) } func TestFile(t *testing.T) { bytes, err := helperLoadFile(t, "some_file") //... }

Slide 46

Slide 46 text

test helpers ✅ func setUp(t *testing.T) { // ... } func tearDown(t *testing.T) { // ... } func TestSomething(t *testing.T) { setUp(t) defer tearDown(t) //... }

Slide 47

Slide 47 text

test helpers ❌ func TestDeferFatal(t *testing.T) { defer helperFatal(t) t.Fatal("Test fatal") } func helperFatal(t *testing.T) { log.Fatal("Helper fatal") }

Slide 48

Slide 48 text

test helpers ❌ func TestDeferFatal(t *testing.T) { defer helperFatal(t) t.Fatal("Test fatal") } func helperFatal(t *testing.T) { log.Fatal("Helper fatal") } $ go test ./test --test.run=TestDeferFatal 2018/02/05 14:03:54 FATAL: Helper fatal

Slide 49

Slide 49 text

testing.go ⚠ Переиспользование в других пакетах Это НЕ тестовый код

Slide 50

Slide 50 text

black box package foo_test

Slide 51

Slide 51 text

build tags ✅ // +build integration

Slide 52

Slide 52 text

build tags ✅ go test -tags=integration

Slide 53

Slide 53 text

build tags ✅ *_integration_test.go package integration_test

Slide 54

Slide 54 text

build tags ✅ doc.go // Этот пакет содержит функциональные интеграционные тесты, // тестирующие приложение по методу черного ящика. // Для его работы необходимы: // … package integration_test

Slide 55

Slide 55 text

build tags ✅ ├── helm │ ├── templates │ │ ├── tests │ │ │ └── runner-pod.yaml │ │ ├── configmap.yaml │ │ └── deployment.yaml │ ├── values.test.yaml │ ├── values.yaml │ └── Chart.yaml └── .

Slide 56

Slide 56 text

build tags ✅ apiVersion: v1 kind: Pod metadata: name: {{ .Release.Name }}-runner labels: service: {{ .Release.Name }}-runner app: test-runner purpose: test spec: restartPolicy: Never containers: - name: service-test imagePullPolicy: IfNotPresent image: {{ .Values.global.imageRegistry }}/service/{{ .Chart.Name }}:{{ .Values.imageTag }} ports: - containerPort: 8090 env: - name: SERVICE_ADDRESS value: {{ .Release.Name }}:8890 - name: MONGO_CONNECTION value: {{ .Release.Name }}-mongo:27017 command: ["go", "test", "-tags=integration"]

Slide 57

Slide 57 text

open source

Slide 58

Slide 58 text

go test ➜ go test ./pkg ok _/Users/amakhov/Documents/testing/testing_in_go/pkg 0.006s

Slide 59

Slide 59 text

go test -v ➜ go test -v ./pkg === RUN TestSum --- PASS: TestSum (0.00s) === RUN TestSum2 --- PASS: TestSum2 (0.00s) === RUN TestSum3 --- PASS: TestSum3 (0.00s) === RUN TestSum4 --- PASS: TestSum4 (0.00s) === RUN TestSum5 --- PASS: TestSum5 (0.00s) === RUN TestSum6 --- PASS: TestSum6 (0.00s) === RUN TestSum7 --- PASS: TestSum7 (0.00s) PASS ok _/Users/amakhov/Documents/testing/testing_in_go/pkg 0.006s

Slide 60

Slide 60 text

go test -v ➜ go test -v ./pkg === RUN TestSum --- PASS: TestSum (0.00s) === RUN TestSum2 --- FAIL: TestSum2 (0.00s) === RUN TestSum3 --- PASS: TestSum3 (0.00s) === RUN TestSum4 --- PASS: TestSum4 (0.00s) === RUN TestSum5 --- PASS: TestSum5 (0.00s) === RUN TestSum6 --- PASS: TestSum6 (0.00s) === RUN TestSum7 --- PASS: TestSum7 (0.00s) FAIL exit status 1 FAIL _/Users/amakhov/Documents/testing/testing_in_go/pkg 0.005s

Slide 61

Slide 61 text

richgo github.com/kyoh86/richgo (https://github.com/kyoh86/richgo)

Slide 62

Slide 62 text

richgo test -v ➜ richgo test -v ./pkg START| Sum PASS | Sum (0.00s) START| Sum2 FAIL | --- FAIL: TestSum2 (0.00s) START| Sum3 PASS | Sum3 (0.00s) START| Sum4 PASS | Sum4 (0.00s) START| Sum5 PASS | Sum5 (0.00s) START| Sum6 PASS | Sum6 (0.00s) START| Sum7 PASS | Sum7 (0.00s) | exit status 1 FAIL _/Users/amakhov/Documents/testing/testing_in_go/pkg 0.005s

Slide 63

Slide 63 text

richgo test -v

Slide 64

Slide 64 text

junit.xml github.com/jstemmer/go-junit-report (https://github.com/jstemmer/go-junit-report) $ go test -v 2>&1 | go-junit-report > report.xml

Slide 65

Slide 65 text

testify/assert github.com/stretchr/testify/assert (https://github.com/stretchr/testify/) func TestSomething(t *testing.T) { // assert equality assert.Equal(t, 123, 123, "they should be equal") // assert inequality assert.NotEqual(t, 123, 456, "they should not be equal") // assert for nil (good for errors) assert.Nil(t, object) // assert for not nil (good when you expect something) if assert.NotNil(t, object) { // now we know that object isn't nil, we are safe to make // further assertions without causing any errors assert.Equal(t, "Something", object.Value) } }

Slide 66

Slide 66 text

testify/require github.com/stretchr/testify/require (https://github.com/stretchr/testify/) func TestSomething(t *testing.T) { // assert equality require.Equal(t, 123, 123, "they should be equal") // assert inequality require.NotEqual(t, 123, 456, "they should not be equal") // assert for nil (good for errors) require.Nil(t, object) // assert for not nil (good when you expect something) if require.NotNil(t, object) { // now we know that object isn't nil, we are safe to make // further assertions without causing any errors require.Equal(t, "Something", object.Value) } }

Slide 67

Slide 67 text

no testify/assert func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { if !condition { _, file, line, _ := runtime.Caller(1) fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line} tb.FailNow() } } func noErr(tb testing.TB, err error) { if err != nil { _, file, line, _ := runtime.Caller(1) fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Err tb.FailNow() } } func equals(tb testing.TB, exp, act interface{}) { if !reflect.DeepEqual(exp, act) { _, file, line, _ := runtime.Caller(1) fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, tb.FailNow() } }

Slide 68

Slide 68 text

Mocks

Slide 69

Slide 69 text

testify/mock github.com/stretchr/testify/mock (https://github.com/stretchr/testify/)

Slide 70

Slide 70 text

testify/mock package app import ( "testing" "github.com/stretchr/testify/mock" ) type MyMockedObject struct{ mock.Mock } func (m *MyMockedObject) DoSomething(number int) (bool, error) { args := m.Called(number) return args.Bool(0), args.Error(1) } func TestSomething(t *testing.T) { testObj := new(MyMockedObject) testObj.On("DoSomething", 123).Return(true, nil) targetFuncThatDoesSomethingWithObj(testObj) testObj.AssertExpectations(t) }

Slide 71

Slide 71 text

minimock github.com/gojuno/minimock (https://github.com/gojuno/minimock/)

Slide 72

Slide 72 text

minimock $ minimock -i github.com/gojuno/minimock/tests.Formatter -o ./tests/

Slide 73

Slide 73 text

minimock //FormatterMock implements github.com/gojuno/minimock/tests.Formatter type FormatterMock struct { t minimock.Tester FormatFunc func(p string, p1 ...interface{}) (r string) FormatCounter uint64 FormatMock mFormatterMockFormat } formatterMock := NewFormatterMock(mc) formatterMock.FormatFunc = func(string, ...interface{}) string { return "minimock" }

Slide 74

Slide 74 text

minimock //FormatterMock implements github.com/gojuno/minimock/tests.Formatter type FormatterMock struct { t minimock.Tester FormatFunc func(p string, p1 ...interface{}) (r string) FormatCounter uint64 FormatMock mFormatterMockFormat } formatterMock := NewFormatterMock(mc) formatterMock.FormatMock.Expect("%s %d", "string", 1).Return("minimock")

Slide 75

Slide 75 text

go-sqlmock github.com/DATA-DOG/go-sqlmock (https://github.com/DATA-DOG/go-sqlmock/)

Slide 76

Slide 76 text

go-sqlmock func TestShouldUpdateStats(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer db.Close() mock.ExpectBegin() mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, mock.ExpectCommit() // now we execute our method if err = recordStats(db, 2, 3); err != nil { t.Errorf("error was not expected while updating stats: %s", err) } // we make sure that all expectations were met if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }

Slide 77

Slide 77 text

Заключение

Slide 78

Slide 78 text

2 задачи тестов доказывать корректность помогать писать код

Slide 79

Slide 79 text

Thank you Алексей Махов Avito @makhov (http://twitter.com/makhov) [email protected] (mailto:[email protected])

Slide 80

Slide 80 text

No content