Upgrade to Pro — share decks privately, control downloads, hide ads and more …

«Введение в тестирование в Go» — Алексей Махов (Avito)

Badoo Tech
March 09, 2018
8.8k

«Введение в тестирование в Go» — Алексей Махов (Avito)

Выступление на Go 1.10 Release Party @ Badoo 24.02.2018.
Пройдем от азов и пакета testing к более сложным инструментам, практикам и tips&tricks.

Badoo Tech

March 09, 2018
Tweet

More Decks by Badoo Tech

Transcript

  1. 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() }
  2. 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() }
  3. *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() }
  4. *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() }
  5. *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() }
  6. Табличные тесты 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) } }) } }
  7. Табличные тесты 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) } }) } }
  8. Табличные тесты 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) } }) } }
  9. Табличные тесты ❌ 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) } }) } }
  10. Табличные тесты ✅ 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) } }) } }
  11. Табличные тесты ✅ 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) } }) } }
  12. Табличные тесты ✅ --- PASS: TestTime (0.00s) --- PASS: TestTime/#00

    (0.00s) --- PASS: TestTime/#01 (0.00s) --- PASS: TestTime/#02 (0.00s)
  13. Табличные тесты ✅ 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) } }) } }
  14. httptest.ResponseRecorder func ExampleResponseRecorder() { 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, _ := 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 // <html><body>Hello World!</body></html> }
  15. httptest.ResponseRecorder func ExampleResponseRecorder() { 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, _ := 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 // <html><body>Hello World!</body></html> }
  16. httptest.ResponseRecorder func ExampleResponseRecorder() { 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, _ := 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 // <html><body>Hello World!</body></html> }
  17. 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 }
  18. 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 }
  19. 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 }
  20. 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
  21. 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
  22. 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! } }
  23. 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") //... }
  24. test helpers ✅ func setUp(t *testing.T) { // ... }

    func tearDown(t *testing.T) { // ... } func TestSomething(t *testing.T) { setUp(t) defer tearDown(t) //... }
  25. test helpers ❌ func TestDeferFatal(t *testing.T) { defer helperFatal(t) t.Fatal("Test

    fatal") } func helperFatal(t *testing.T) { log.Fatal("Helper fatal") }
  26. 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
  27. build tags ✅ doc.go // Этот пакет содержит функциональные интеграционные

    тесты, // тестирующие приложение по методу черного ящика. // Для его работы необходимы: // … package integration_test
  28. build tags ✅ ├── helm │ ├── templates │ │

    ├── tests │ │ │ └── runner-pod.yaml │ │ ├── configmap.yaml │ │ └── deployment.yaml │ ├── values.test.yaml │ ├── values.yaml │ └── Chart.yaml └── .
  29. 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"]
  30. 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
  31. 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
  32. 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
  33. 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) } }
  34. 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) } }
  35. 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() } }
  36. 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) }
  37. 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" }
  38. 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")
  39. 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) } }