Тестирование

Лекция 5

Максим Иванов

go test

2

Тестовые функции

Тестовые функции должны иметь сигнатуру:

func TestName(t *testing.T) {
    // ...
}

Параметр *testing.T используется, чтобы сообщить о падении теста.

3

Пример Palindrome

// 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
4

Пример Palindrome

// 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
5

Пример Palindrome

// 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
}

//!-
6

Table Driven Test

// 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)
}
*/
7

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) {}
8

fmt isSpace example

`
// export_test.go
package fmt

var IsSpace = isSpace

отличаются в поведении.

9

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` после страниц стектрейсов.

10

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)
    }
    // ...
}
11

Завершение теста

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")
    }
}
12

Завершение теста

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)
13

Тестовые проверки в других горутинах

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()
}
14

Горутины и завершение теста

func TestGo(t *testing.T) {
    go func() {
        for {
            time.Sleep(time.Second)
            t.Logf("tick") // This will panic
        }
    }()
}
15

Правильное завершение

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")
        }
    }()
}
16

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
17

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)
18

testify

func TestCall(t *testing.T) {
    res, err := Call()
    require.NoError(t, err)

    assert.Equal(t, 42, res)
}
19

Coverage

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"
}
20

Coverage

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
21

Coverage

go test -coverprofile=coverage.out
go tool cover -html=coverage.out
22

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
23

Запуск только бенчмарков

$ go test -bench=. -run=^$
$ go test -bench='^BenchmarkIsPalindrome$' -run=^$
$ go test ./... -bench=. -run=^$
24

Подготовка данных в бенчмарках

// Единовременная подготовка до измерений
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)
    }
}
25

Бенчмарки и аллокации

func BenchmarkAlloc(b *testing.B) {
    b.ReportAllocs()
    for i := 0; i < b.N; i++ {
        _ = bytes.Repeat([]byte("x"), 10)
    }
}

$ go test -bench=. -benchmem -run=^$
26

b.N и длительность бенчмарка

$ go test -bench=. -benchtime=3s -run=^$
$ go test -bench=BenchmarkDoWork -benchtime=100x -run=^$
27

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)
        })
    }
}

$ go test -bench='^BenchmarkSizes/100$' -run=^$
$ go test -bench='BenchmarkSizes/(100|1000)$' -run=^$
28

Анализ результатов бенчмарков

$ go test -bench=. -benchmem -run=^$ > old.txt
$ go test -bench=. -benchmem -run=^$ > new.txt
$ benchstat old.txt new.txt
29

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)
}
30

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)
        }
    })
}
31

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("泃")
32

Parallel tests

func TestA(t *testing.T) {
    t.Parallel()
    // ...
}

func TestB(t *testing.T) {
    t.Parallel()
    // ...
}

func TestC(t *testing.T) {
    // ...
}
33

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
}
34

TestMain

Иногда нужно сделать глобальную инициализацию.

func TestMain(m *testing.M) {
    if os.Getenv("INSIDE_DOCKER") == "" {
        os.Exit(runSelfInDocker())
    }

    os.Exit(m.Run())
}
35

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
36

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
37

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")
    }
}

Иногда полезно пропускать тесты, которые используют внешние зависимости.

38

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()

    // ...
}
39

t.Cleanup()

func newEnv(t *testing.T) *env {
    // ...
    t.Cleanup(func() {
        DB.Close()
    })
}

func TestA(t *testing.T) {
    env := newEnv(t)
    // ...
}
40

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()
    }
}
41

Race detector

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
}
42

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/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
43

Пример Echo

// 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
}
44

Пример Echo

// 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)
        }
    }
}
45

White box testing

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)
    }
}
46

White box testing

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)
    }
}
47

gomock

package example

//go:generate mockgen -package example -destination mock.go . Foo

type Foo interface {
    Bar(x int) int
}

func SUT(f Foo) {
    // ...
}
48

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)
}
49

httptest

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))
}
50

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())
}
51

Thank you

Максим Иванов

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)