Testing in Go

Who am I?

Damiano Petrungaro

Damiano Petrungaro

I know... I look younger and thinner in this pic, but HEY DO NOT JUDGE ME!

Italy

source: www.vidiani.com

source: www.romaest.org

Me everyday:

Testing

Using the "testing" pkg

package testing

// TB is the interface common to T and B.
type TB interface {
	Cleanup(func())
	Error(args ...interface{})
	Errorf(format string, args ...interface{})
	Fail()
	FailNow()
	Failed() bool
	Fatal(args ...interface{})
	Fatalf(format string, args ...interface{})
	Helper()
	Log(args ...interface{})
	Logf(format string, args ...interface{})
	Name() string
	Skip(args ...interface{})
	SkipNow()
	Skipf(format string, args ...interface{})
	Skipped() bool
	TempDir() string
}

var _ TB = (*T)(nil)
var _ TB = (*B)(nil)

Using the "testing" pkg

package testing

// B is a type passed to Benchmark functions to manage benchmark
// timing and to specify the number of iterations to run.
//
// A benchmark ends when its Benchmark function returns or calls any of the methods
// FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods must be called
// only from the goroutine running the Benchmark function.
// The other reporting methods, such as the variations of Log and Error,
// may be called simultaneously from multiple goroutines.
//
// Like in tests, benchmark logs are accumulated during execution
// and dumped to standard output when done. Unlike in tests, benchmark logs
// are always printed, so as not to hide output whose existence may be
// affecting benchmark results.
type B struct {
	common
	importPath       string // import path of the package containing the benchmark
	context          *benchContext
	N                int
	previousN        int           // number of iterations in the previous run
	previousDuration time.Duration // total duration of the previous run
	benchFunc        func(b *B)
	benchTime        benchTimeFlag
	bytes            int64
	missingBytes     bool // one of the subbenchmarks does not have bytes set.
	timerOn          bool
	showAllocResult  bool
	result           BenchmarkResult
	parallelism      int // RunParallel creates parallelism*GOMAXPROCS goroutines
	// The initial states of memStats.Mallocs and memStats.TotalAlloc.
	startAllocs uint64
	startBytes  uint64
	// The net total of this test after being run.
	netAllocs uint64
	netBytes  uint64
	// Extra metrics collected by ReportMetric.
	extra map[string]float64
}

Using the "testing" pkg

package testing

// T is a type passed to Test functions to manage test state and support formatted test logs.
//
// A test ends when its Test function returns or calls any of the methods
// FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods, as well as
// the Parallel method, must be called only from the goroutine running the
// Test function.
//
// The other reporting methods, such as the variations of Log and Error,
// may be called simultaneously from multiple goroutines.
type T struct {
	common
	isParallel bool
	context    *testContext // For running tests and subtests.
}

Testing semantics

and basic mechanisms

$ go help test     
usage: go test [build/test flags] [packages] [build/test flags & test binary flags]

'Go test' automates testing the packages named by the import paths.
It prints a summary of the test results in the format:

        ok   archive/tar   0.011s
        FAIL archive/zip   0.022s
        ok   compress/gzip 0.033s
        ...

followed by detailed output for each failed package.

'Go test' recompiles each package along with any files with names matching
the file pattern "*_test.go".
These additional files can contain test functions, benchmark functions, and
example functions. See 'go help testfunc' for more.
Each listed package causes the execution of a separate test binary.
Files whose names begin with "_" (including "_test.go") or "." are ignored.

Test files that declare a package with the suffix "_test" will be compiled as a
separate package, and then linked and run with the main test binary.

The go tool will ignore a directory named "testdata", making it available
to hold ancillary data needed by the tests.

As part of building a test binary, go test runs go vet on the package
and its test source files to identify significant problems. If go vet
finds any problems, go test reports those and does not run the test
binary. Only a high-confidence subset of the default go vet checks are
used. That subset is: 'atomic', 'bool', 'buildtags', 'errorsas',
'ifaceassert', 'nilfunc', 'printf', and 'stringintconv'. You can see
the documentation for these and other vet tests via "go doc cmd/vet".
To disable the running of go vet, use the -vet=off flag.

All test output and summary lines are printed to the go command's
standard output, even if the test printed them to its own standard
error. (The go command's standard error is reserved for printing
errors building the tests.)

Go test runs in two different modes:

The first, called local directory mode, occurs when go test is
invoked with no package arguments (for example, 'go test' or 'go
test -v'). In this mode, go test compiles the package sources and
tests found in the current directory and then runs the resulting
test binary. In this mode, caching (discussed below) is disabled.
After the package test finishes, go test prints a summary line
showing the test status ('ok' or 'FAIL'), package name, and elapsed
time.

The second, called package list mode, occurs when go test is invoked
with explicit package arguments (for example 'go test math', 'go
test ./...', and even 'go test .'). In this mode, go test compiles
and tests each of the packages listed on the command line. If a
package test passes, go test prints only the final 'ok' summary
line. If a package test fails, go test prints the full test output.
If invoked with the -bench or -v flag, go test prints the full
output even for passing package tests, in order to display the
requested benchmark results or verbose logging. After the package
tests for all of the listed packages finish, and their output is
printed, go test prints a final 'FAIL' status if any package test
has failed.

In package list mode only, go test caches successful package test
results to avoid unnecessary repeated running of tests. When the
result of a test can be recovered from the cache, go test will
redisplay the previous output instead of running the test binary
again. When this happens, go test prints '(cached)' in place of the
elapsed time in the summary line.

The rule for a match in the cache is that the run involves the same
test binary and the flags on the command line come entirely from a
restricted set of 'cacheable' test flags, defined as -cpu, -list,
-parallel, -run, -short, and -v. If a run of go test has any test
or non-test flags outside this set, the result is not cached. To
disable test caching, use any test flag or argument other than the
cacheable flags. The idiomatic way to disable test caching explicitly
is to use -count=1. Tests that open files within the package's source
root (usually $GOPATH) or that consult environment variables only
match future runs in which the files and environment variables are unchanged.
A cached test result is treated as executing in no time at all,
so a successful package test result will be cached and reused
regardless of -timeout setting.

In addition to the build flags, the flags handled by 'go test' itself are:

        -args
            Pass the remainder of the command line (everything after -args)
            to the test binary, uninterpreted and unchanged.
            Because this flag consumes the remainder of the command line,
            the package list (if present) must appear before this flag.

        -c
            Compile the test binary to pkg.test but do not run it
            (where pkg is the last element of the package's import path).
            The file name can be changed with the -o flag.

        -exec xprog
            Run the test binary using xprog. The behavior is the same as
            in 'go run'. See 'go help run' for details.

        -i
            Install packages that are dependencies of the test.
            Do not run the test.

        -json
            Convert test output to JSON suitable for automated processing.
            See 'go doc test2json' for the encoding details.

        -o file
            Compile the test binary to the named file.
            The test still runs (unless -c or -i is specified).

The test binary also accepts flags that control execution of the test; these
flags are also accessible by 'go test'. See 'go help testflag' for details.

For more about build flags, see 'go help build'.
For more about specifying packages, see 'go help packages'.

See also: go build, go vet.

Testing semantics

and basic mechanisms

$ go help test     
'Go test' tests things.

'Go test' run files matching a pattern "*_test.go", ignoring the files that starts with a "." or an "_".

You can also add a suffix "_test" in the package name if you wanna compile as a separate one,
those will be then linked and run with the main test binary.

'Go test' will ignore directories named "testdata", so please put here ancillary data needed by the tests.

'Go test' will run go vet as well as part of it, and won't execute tests if it fails,
to disable the running of go vet, use the -vet=off flag.

'Go test' prints the output to stdout, if test won't run due to a problem, then the output is printed to stderr.

'Go test' runs in two ways:
- local directory mode 	AKA "go test": runs for files in the current directory and does not cache the results.
- package list mode 	AKA "go test ./...": runs for all listed packages and does cache the results.

If you wanna disable cache add a "-count=1" flag to your command.

'Go test' compile testing binaries and then run the in parallel depending on the number of available CPUs.

If you wanna see a compiled version of your package test, you can run "go test ./yourpackage -c".
You can only do it one package at the time.

A Dam's summary with some extra info

package presentation

import "testing"

func TestXxx(t *testing.T) {
    // here you write yout tesging logic
}

Testing semantics

and basic mechanisms

package presentation

import "testing"

func TestXxx(t *testing.T) {
    // here you write your testing logic
}

func TestYyy(t *testing.T) {
    // here you write your testing logic
}

Testing semantics

and basic mechanisms

$ go test presentation_test.go
ok      command-line-arguments  0.291s
package presentation

import "testing"

func TestXxx(t *testing.T) {
    // here you write your testing logic
}

func TestYyy(t *testing.T) {
    // here you write your testing logic
}

Testing semantics

and basic mechanisms

$ go test presentation_test.go -v
=== RUN   TestXxx
--- PASS: TestXxx (0.00s)
=== RUN   TestYyy
--- PASS: TestYyy (0.00s)
PASS
ok      command-line-arguments  0.069s
package presentation

import "testing"

func TestXxx(t *testing.T) {
    // here you write your testing logic
}

func TestYyy(t *testing.T) {
    // here you write your testing logic
}

Testing semantics

and basic mechanisms

$ go test presentation_test.go -v -run TestX
=== RUN   TestXxx
--- PASS: TestXxx (0.00s)
PASS
ok      command-line-arguments  0.070s
package presentation

import "testing"

func TestXxx(t *testing.T) {
    // here you write your testing logic
}

func TestYyy(t *testing.T) {
    // here you write your testing logic
}

Testing semantics

and basic mechanisms

$ go test presentation_test.go -v -run TestY
=== RUN   TestYyy
--- PASS: TestYyy (0.00s)
PASS
ok      command-line-arguments  0.066s
package presentation

import "testing"

func TestLog(t *testing.T) {
	a, b := "a", "b"
	t.Log("Hello!")
	t.Logf("Log: %s", a)
	t.Logf("Log: %s", b)
}

Log & Logf

$ go test presentation_test.go -v
=== RUN   TestLog
    presentation_test.go:9: Hello!
    presentation_test.go:10: Log: a
    presentation_test.go:11: Log: b
--- PASS: TestLog (0.00s)
PASS
ok      command-line-arguments  0.196s
package presentation

import "testing"

func TestNameOne(t *testing.T) {
	t.Log(t.Name())
}

func TestNameTwo(t *testing.T) {
	t.Log(t.Name())
}

Name

$ go test presentation_test.go -v
=== RUN   TestNameOne
    presentation_test.go:6: TestNameOne
--- PASS: TestNameOne (0.00s)
=== RUN   TestNameTwo
    presentation_test.go:10: TestNameTwo
--- PASS: TestNameTwo (0.00s)
PASS
ok      command-line-arguments
package presentation

import "testing"

func TestCleanup(t *testing.T) {
	t.Log("Before")
	t.Cleanup(func() { t.Log("1") })
	t.Cleanup(func() { t.Log("2") })
	t.Log("After")
}

Cleanup

$ go test presentation_test.go -v
=== RUN   Cleanup
    presentation_test.go:8: Before
    presentation_test.go:11: After
    presentation_test.go:10: 2
    presentation_test.go:9: 1
--- PASS: Cleanup (0.00s)
PASS
ok      command-line-arguments  0.174s
package presentation

import "testing"

func TestError(t *testing.T) {
	a, b := "a", "b"
	t.Error("Hello!")
	t.Errorf("Error: %s", a)
	t.Errorf("Error: %s", b)
}

Error & Errorf

$ go test presentation_test.go -v
=== RUN   TestError
    presentation_test.go:7: Hello!
    presentation_test.go:8: Error: a
    presentation_test.go:9: Error: b
--- FAIL: TestError (0.00s)
FAIL
ok      command-line-arguments  0.121s
package presentation

import "testing"

func TestFatal(t *testing.T) {
	t.Log("Before")
	t.Fatal("Hello!")
	t.Log("After")
}

func TestFatalf(t *testing.T) {
	a := "a"
	t.Log("Before")
	t.Fatalf("Fatal: %s", a)
	t.Log("After")
}

Fatal & Fatalf

$ go test presentation_test.go -v
=== RUN   TestFatal
    presentation_test.go:6: Before
    presentation_test.go:7: Hello!
--- FAIL: TestFatal (0.00s)
=== RUN   TestFatalf
    presentation_test.go:13: Before
    presentation_test.go:14: Fatal: a
--- FAIL: TestFatalf (0.00s)
FAIL
FAIL    command-line-arguments  0.262s
FAIL
package presentation

import "testing"

func TestFail(t *testing.T) {
	t.Log("Before")
	t.Fail()
	t.Log("After")
}

Fail

$ go test presentation_test.go -v
=== RUN   TestFail
    presentation_test.go:6: Before
    presentation_test.go:8: After
--- FAIL: TestFail (0.00s)
FAIL
FAIL    command-line-arguments  0.258s
FAIL
package presentation

import "testing"

func TestFailNow(t *testing.T) {
	t.Log("Before")
	t.FailNow()
	t.Log("After")
}

FailNow

$ go test presentation_test.go -v
=== RUN   TestFailNow
    presentation_test.go:6: Before
--- FAIL: TestFailNow (0.00s)
FAIL
FAIL    command-line-arguments  0.073s
FAIL
package presentation

import "testing"

func TestFailed(t *testing.T) {
	t.Log(t.Failed())
	t.Cleanup(func() { t.Log(t.Failed()) })
	t.Fail()
}

Failed

$ go test presentation_test.go -v
=== RUN   TestFailed
    presentation_test.go:6: false
    presentation_test.go:7: true
--- FAIL: TestFailed (0.00s)
FAIL
FAIL    command-line-arguments  0.174s
FAIL
package presentation

import "testing"

func TestSkip(t *testing.T) {
	t.Log("Before")
	t.Skip() // you can also call t.Skip("args")
	t.Log("After")
}

func TestSkipf(t *testing.T) {
	name := t.Name()
	t.Log("Before")
	t.Skipf("skipping: %s", name)
	t.Log("After")
}

Skip & Skipf

$ go test presentation_test.go -v -run ^TestSkip$
=== RUN   TestSkip
    presentation_test.go:6: Before
    presentation_test.go:7: 
--- SKIP: TestSkip (0.00s)
PASS
ok      command-line-arguments  0.061s


$ go test presentation_test.go -v -run TestSkipf
=== RUN   TestSkipf
    presentation_test.go:13: Before
    presentation_test.go:14: skipping: TestSkipf
--- SKIP: TestSkipf (0.00s)
PASS
ok      command-line-arguments  0.090s
package presentation

import "testing"

func TestSkipNow(t *testing.T) {
	t.Log("Before")
	t.SkipNow()
	t.Log("After")
}

func TestSkipNowAfterError(t *testing.T) {
	t.Error("Before")
	t.SkipNow()
	t.Log("After")
}

SkipNow

$ go test presentation_test.go -v -run ^TestSkipNow$
=== RUN   TestSkipNow
    presentation_test.go:6: Before
--- SKIP: TestSkipNow (0.00s)
PASS
ok      command-line-arguments  0.067s


$ go test presentation_test.go -v -run TestSkipNowAfterError
=== RUN   TestSkipNowAfterError
    presentation_test.go:12: Before
--- FAIL: TestSkipNowAfterError (0.00s)
FAIL
FAIL    command-line-arguments  0.065s
FAIL
package presentation

import "testing"

func TestSkipped(t *testing.T) {
	t.Log(t.Skipped())
	t.Cleanup(func() { t.Log(t.Skipped()) })
	t.Skip()
}

Skipped

$ go test presentation_test.go -v
=== RUN   TestSkipped
    presentation_test.go:6: false
    presentation_test.go:8: 
    presentation_test.go:7: true
--- SKIP: TestSkipped (0.00s)
PASS
ok      command-line-arguments  0.173s
package presentation

import "testing"

func TestXxx(t *testing.T) {
	HelperFn(t)
	HelperFn(t)
   	HelperFn(t)
}

func HelperFn(t *testing.T) {
	t.Helper()
	t.Log("Hello!")
}

Helper

$ go test presentation_test.go -v
=== RUN   TestXxx
    presentation_test.go:6: Hello!
    presentation_test.go:7: Hello!
    presentation_test.go:8: Hello!
--- PASS: TestXxx (0.00s)
PASS
ok      command-line-arguments  0.174s
package presentation

import (
	"os"
	"testing"
)

func TestTempDir(t *testing.T) {
	var dir string
	t.Cleanup(func() {
		HelperFunc(t, "After", dir)
	})
	dir = t.TempDir()
	HelperFunc(t, "Before", dir)
}

func HelperFunc(t *testing.T, fmt, dir string) {
	t.Helper()
	_, err := os.Stat(dir)
	t.Logf("%s: %t", fmt, !os.IsNotExist(err))
}

TempDir

$ go test presentation_test.go -v
=== RUN   TestTempDir
    presentation_test.go:16: Before: true
    presentation_test.go:12: After: false
--- PASS: TestTempDir (0.00s)
PASS
ok      command-line-arguments  0.315s

Test execution

$ go test ./...

src/cmd/go/internal/test/test.go:568

Test execution

package test
// ... 
var pkgs []*load.Package

// ... 
func init() {
	CmdTest.Run = runTest
}

// ... 
func runTest(){
	var b work.Builder
	b.Init()

	var builds, runs, prints []*Action
    for _, pkg := range pkgs {
    	builds = append(builds, build(pkg))
        runs = append(runs, run(pkg))
		prints = append(prints, print(pkg))
    }

    root := &work.Action{Mode: "go test", Func: printExitStatus, Deps: builds+runs+prints}
    b.Do(root) // execute in parallel the packages
}

Test execution

package yourpackage

// .... a lot of code here

func TestMain(m *testing.M) {
	os.Exit(m.Run())
}

// .... a lot of code here

Test execution

package testing

// .... a lot of code here

func (m *M) Run() (code int) {
	// ...code here
	flag.Parse()
	// ....code here
	parseCpuList()
	// ....code here
	testRan, testOk := runTests(m.deps.MatchString, m.tests, deadline)
	if !testRan {
		fmt.Fprintln(os.Stderr, "testing: warning: no tests to run")
	}
	if !testOk || race.Errors() > 0 {
		fmt.Println("FAIL")
		m.exitCode = 1
		return
	}

	fmt.Println("PASS")
	m.exitCode = 0
	return
}

Test execution

package testing

// .... a lot of code here

func runTests(
	matchString func(str string) (bool, error),
	tests []InternalTest,
	deadline time.Time,
) (ran, ok bool) {
	for _, procs := range cpuList {
			ctx := newTestContext(*parallel, matchString)
			ctx.deadline = deadline
			t := &T{...}
			tRunner(t, func(t *T) {
				for _, test := range tests {
					t.Run(test.Name, test.F)
				}
				go func() { <-t.signal }()
			})
			ok = ok && !t.Failed()
			ran = ran || t.ran
		}
	}
	return ran, ok
}

Test execution

package testing

// .... a lot of code here

func tRunner(t *T, fn func(t *T)) {
	defer func() {
		if t.Failed() {
			atomic.AddUint32(&numFailed, 1)
		}

		if t.raceErrors+race.Errors() > 0 {
			t.Errorf("race detected during execution of test")
		}

		t.duration += time.Since(t.start)

		if len(t.sub) > 0 {
			for _, sub := range t.sub {
				<-sub.signal
			}
			if !t.isParallel {
				t.context.waitParallel()
			}
		} else if t.isParallel {
			t.context.release()
		}
		t.report() // Report after all subtests have finished.
		t.done = true
		t.signal <- signal
	}()

	t.start = time.Now()
	fn(t)

	t.finished = true
}

Test execution

package testing

// .... a lot of code here

func (t *T) Run(name string, f func(t *T)) bool {
	t = &T{
		common: common{
			barrier: make(chan bool),
			signal:  make(chan bool),
			name:    testName,
			parent:  &t.common,
			level:   t.level + 1,
			creator: pc[:n],
			chatty:  t.chatty,
		},
		context: t.context,
	}

	if t.chatty {
		printer.Fprint(root.w, t.name, fmt.Sprintf("=== RUN   %s\n", t.name))
	}
	go tRunner(t, f)
	if !<-t.signal {
		runtime.Goexit()
	}
	return !t.failed
}

Test execution

$ go test ./... -v
=== RUN   TestClient
=== RUN   TestClient/not_found
=== PAUSE TestClient/not_found
=== RUN   TestClient/server_error
=== PAUSE TestClient/server_error
=== RUN   TestClient/existing_user
=== PAUSE TestClient/existing_user
=== CONT  TestClient/not_found
=== CONT  TestClient/server_error
=== CONT  TestClient/existing_user
--- PASS: TestClient (0.00s)
    --- PASS: TestClient/server_error (0.00s)
    --- PASS: TestClient/existing_user (0.00s)
    --- PASS: TestClient/not_found (0.00s)
PASS
ok      github.com/damianopetrungaro/presentation/client        0.102s
=== RUN   TestStart
=== RUN   TestStart/not_found
=== PAUSE TestStart/not_found
=== RUN   TestStart/health
=== PAUSE TestStart/health
=== RUN   TestStart/users
=== PAUSE TestStart/users
=== CONT  TestStart/not_found
=== CONT  TestStart/users
=== CONT  TestStart/health
--- PASS: TestStart (0.00s)
    --- PASS: TestStart/not_found (0.51s)
    --- PASS: TestStart/health (0.51s)
    --- PASS: TestStart/users (0.51s)
PASS
ok      github.com/damianopetrungaro/presentation/server        0.736s
=== RUN   TestStrings
=== RUN   TestStrings/Pipe
=== RUN   TestStrings/none
=== RUN   TestStrings/WrapWithPipes
=== RUN   TestStrings/WrapWithUnderscores
=== RUN   TestStrings/WrapWithDashes
=== RUN   TestStrings/RemoveDashes
--- PASS: TestStrings (0.00s)
    --- PASS: TestStrings/Pipe (0.00s)
    --- PASS: TestStrings/none (0.00s)
    --- PASS: TestStrings/WrapWithPipes (0.00s)
    --- PASS: TestStrings/WrapWithUnderscores (0.00s)
    --- PASS: TestStrings/WrapWithDashes (0.00s)
    --- PASS: TestStrings/RemoveDashes (0.00s)
PASS
ok      github.com/damianopetrungaro/presentation/strings       0.170s

Test execution

$ go test ./...
ok      github.com/damianopetrungaro/presentation/client        0.190s
ok      github.com/damianopetrungaro/presentation/server        0.772s
ok      github.com/damianopetrungaro/presentation/strings       0.178s

$ go test ./...
ok      github.com/damianopetrungaro/presentation/client        (cached)
ok      github.com/damianopetrungaro/presentation/server        (cached)
ok      github.com/damianopetrungaro/presentation/strings       (cached)
go test ./... -count=1
ok      github.com/damianopetrungaro/presentation/client        0.146s
ok      github.com/damianopetrungaro/presentation/server        0.591s
ok      github.com/damianopetrungaro/presentation/strings       0.066s

go test ./... -count=1
ok      github.com/damianopetrungaro/presentation/client        0.124s
ok      github.com/damianopetrungaro/presentation/server        0.611s
ok      github.com/damianopetrungaro/presentation/strings       0.058s

How to write tests

Simple unit-test

package strings

import (
	"fmt"
	"strings"
)

type Manipulate func(string) string

func Pipe(ms ...Manipulate) Manipulate {
	return func(s string) string {
		for _, m := range ms {
			s = m(s)
		}
		return s
	}
}

func WrapWithPipes(s string) string {
	return fmt.Sprintf("|%s|", s)
}

func WrapWithUnderscores(s string) string {
	return fmt.Sprintf("_%s_", s)
}

func WrapWithDashes(s string) string {
	return fmt.Sprintf("-%s-", s)
}

func RemoveDashes(s string) string {
	return strings.ReplaceAll(s, "-", "")
}

How to write tests

unit test

package strings

import (
	"testing"
)

func TestWrapWithPipes(t *testing.T) {
	if output := WrapWithPipes("damiano"); output != "|damiano|" {
		t.Error("could not match strings")
		t.Errorf("got: %s", output)
		t.Errorf("want: %s", "|damiano|")
	}
}

func TestWrapWithUnderscores(t *testing.T) {
	if output := WrapWithUnderscores("damiano"); output != "_damiano_" {
		t.Error("could not match strings")
		t.Errorf("got: %s", output)
		t.Errorf("want: %s", "_damiano_")
	}
}

func TestWrapWithDashes(t *testing.T) {
	if output := WrapWithDashes("damiano"); output != "-damiano-" {
		t.Error("could not match strings")
		t.Errorf("got: %s", output)
		t.Errorf("want: %s", "-damiano-")
	}
}

func TestRemoveDashes(t *testing.T) {
	if output := RemoveDashes("-dam-i-ano-"); output != "damiano" {
		t.Error("could not match strings")
		t.Errorf("got: %s", output)
		t.Errorf("want: %s", "damiano")
	}
}

func TestPipe(t *testing.T) {
	pipe := Pipe(WrapWithPipes,WrapWithUnderscores,WrapWithDashes,RemoveDashes)

	if output := pipe("damiano"); output != "_|damiano|_" {
		t.Error("could not match strings")
		t.Errorf("got: %s", output)
		t.Errorf("want: %s", "_|damiano|_")
	}
}

How to write tests

unit test

$ go test ./strings/... -v
=== RUN   TestWrapWithPipes
--- PASS: TestWrapWithPipes (0.00s)
=== RUN   TestWrapWithUnderscores
--- PASS: TestWrapWithUnderscores (0.00s)
=== RUN   TestWrapWithDashes
--- PASS: TestWrapWithDashes (0.00s)
=== RUN   TestRemoveDashes
--- PASS: TestRemoveDashes (0.00s)
=== RUN   TestPipe
--- PASS: TestPipe (0.00s)
PASS
ok      github.com/damianopetrungaro/presentation/strings       0.070s

How to write tests

unit test

package strings

import (
	"testing"
)

func TestStrings(t *testing.T) {
	tests := map[string]struct {
		input      string
		manipulate Manipulate
		output     string
	}{
		"WrapWithPipes": {
			input:      "damiano",
			manipulate: WrapWithPipes,
			output:     "|damiano|",
		},
		"WrapWithUnderscores": {
			input:      "damiano",
			manipulate: WrapWithUnderscores,
			output:     "_damiano_",
		},
		"WrapWithDashes": {
			input:      "damiano",
			manipulate: WrapWithDashes,
			output:     "-damiano-",
		},
		"RemoveDashes": {
			input:      "-dam-i-ano-",
			manipulate: RemoveDashes,
			output:     "damiano",
		},
		"Pipe": {
			input:      "damiano",
			manipulate: Pipe(WrapWithPipes, WrapWithUnderscores, WrapWithDashes, RemoveDashes),
			output:     "_|damiano|_",
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			output := test.manipulate(test.input)
			if output != test.output {
				t.Error("could not match output")
				t.Errorf("got:%s", output)
				t.Errorf("want:%s", test.output)
			}
		})
	}
}

How to write tests

unit test

$ go test ./strings/... -v
=== RUN   TestStrings
=== RUN   TestStrings/WrapWithPipes
=== RUN   TestStrings/WrapWithUnderscores
=== RUN   TestStrings/WrapWithDashes
=== RUN   TestStrings/RemoveDashes
=== RUN   TestStrings/Pipe
--- PASS: TestStrings (0.00s)
    --- PASS: TestStrings/WrapWithPipes (0.00s)
    --- PASS: TestStrings/WrapWithUnderscores (0.00s)
    --- PASS: TestStrings/WrapWithDashes (0.00s)
    --- PASS: TestStrings/RemoveDashes (0.00s)
    --- PASS: TestStrings/Pipe (0.00s)
PASS
ok      github.com/damianopetrungaro/presentation/strings       0.71s

How to write tests

unit test

$ go test ./strings/... -v -run TestStrings/WrapWithPipes
=== RUN   TestStrings
=== RUN   TestStrings/WrapWithPipes
--- PASS: TestStrings (0.00s)
    --- PASS: TestStrings/WrapWithPipes (0.00s)
PASS
ok      github.com/damianopetrungaro/presentation/strings       0.188s

How to write tests

unit test

How to write tests

integration-test

server side

How to write tests

integration-test (server side)

package server

import (
	"context"
	"errors"
	"fmt"
	"log"
	"net/http"
	"time"
)

const Addr = "127.0.0.1:8080"

func New() *http.Server {
	return &http.Server{Addr: Addr, Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// oh no! this is slow!
		time.Sleep(time.Millisecond * 500)

		if r.URL.Path == "/health" {
			if _, err := fmt.Fprint(w, "ok"); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
			}
			return
		}

		if r.URL.Path == "/users" {
			if _, err := fmt.Fprint(w, `[{"id":1, "name": "Mario"}, {"id":2, "name": "Luigi"}]`); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
			}
			return
		}

		w.WriteHeader(http.StatusNotFound)
	})}
}

func Start(ctx context.Context, ch chan<- struct{}, logger *log.Logger) {
	srv := New()

	go func() {
		logger.Print("server listening")
		ch <- struct{}{}
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			logger.Fatalf("could not keep server served: %s", err)
		}
	}()

	<-ctx.Done()
	if err := srv.Shutdown(ctx); err != nil && !errors.Is(err, context.Canceled) {
		logger.Fatalf("could not shout down server: %s", err)
	}

	ch <- struct{}{}
	logger.Print("server closed")
}

How to write tests

integration-test (server side)

package server

import (
	"bytes"
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestServer(t *testing.T) {
	srv := New()
	
	tests := map[string]struct {
		method     string
		url        string
		statusCode int
		body       []byte
	}{
		"health": {
			method:     http.MethodGet,
			url:        "/health",
			statusCode: http.StatusOK,
			body:       []byte("ok"),
		},
		"users": {
			method:     http.MethodGet,
			url:        "/users",
			statusCode: http.StatusOK,
			body:       []byte(`[{"id":1, "name": "Mario"}, {"id":2, "name": "Luigi"}]`),
		},
		"not found": {
			method:     http.MethodGet,
			url:        "/none",
			statusCode: http.StatusNotFound,
			body:       nil,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			rec := httptest.NewRecorder()
			srv.Handler.ServeHTTP(rec, httptest.NewRequest(test.method, test.url, nil))
			res := rec.Result()

			if res.StatusCode != test.statusCode {
				t.Error("could not match status code")
				t.Errorf("got: %d", res.StatusCode)
				t.Errorf("want: %d", test.statusCode)
			}

			resBody, err := ioutil.ReadAll(res.Body)
			if err != nil {
				t.Fatalf("could not read response body: %s", err)
			}

			if !bytes.Equal(resBody, test.body) {
				t.Error("could not match status code")
				t.Errorf("got: %d", resBody)
				t.Errorf("want: %d", test.body)
			}
		})
	}
}

How to write tests

integration-test (server side)

$ go test ./server/... -v
=== RUN   TestServer
=== RUN   TestServer/health
=== RUN   TestServer/users
=== RUN   TestServer/not_found
--- PASS: TestServer (1.51s)
    --- PASS: TestServer/health (0.50s)
    --- PASS: TestServer/users (0.50s)
    --- PASS: TestServer/not_found (0.50s)
PASS
ok      github.com/damianopetrungaro/presentation/server        1.589s

How to write tests

integration-test (server side)

package server

// ...

func TestServer(t *testing.T) {
	// ...

	for name, test := range tests {
		test := test
		t.Run(name, func(t *testing.T) {
	        /...
        	t.Parallel()
   	        /...
		})
	}
}

How to write tests

integration-test (server side)

$ go test ./server/... -v
=== RUN   TestServer
=== RUN   TestServer/health
=== PAUSE TestServer/health
=== RUN   TestServer/users
=== PAUSE TestServer/users
=== RUN   TestServer/not_found
=== PAUSE TestServer/not_found
=== CONT  TestServer/health
=== CONT  TestServer/not_found
=== CONT  TestServer/users
--- PASS: TestServer (0.00s)
    --- PASS: TestServer/not_found (0.50s)
    --- PASS: TestServer/users (0.50s)
    --- PASS: TestServer/health (0.50s)
PASS
ok      github.com/damianopetrungaro/presentation/server        0.689s

How to write tests

integration-test (server side)

package server

import (
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"testing"
	"time"
)

func TestStart(t *testing.T) {
	ctx, cancelFn := context.WithCancel(context.Background())
	ch := make(chan struct{})

	go func() {
		Start(ctx, ch, log.New(ioutil.Discard, "", log.LstdFlags))
	}()

	select {
	case <-time.Tick(time.Second):
		t.Fatal("could not mark the server as started")
	case <-ch:
	}

	t.Cleanup(func() {
		cancelFn()
		select {
		case <-time.Tick(time.Second):
			t.Error("could not mark the server as shut down")
		case <-ch:
		}
	})

	tests := map[string]struct {
		method     string
		url        string
		statusCode int
		body       []byte
	}{
		"health": {
			method:     http.MethodGet,
			url:        "/health",
			statusCode: http.StatusOK,
			body:       []byte("ok"),
		},
		"users": {
			method:     http.MethodGet,
			url:        "/users",
			statusCode: http.StatusOK,
			body:       []byte(`[{"id":1, "name": "Mario"}, {"id":2, "name": "Luigi"}]`),
		},
		"not found": {
			method:     http.MethodGet,
			url:        "/none",
			statusCode: http.StatusNotFound,
			body:       nil,
		},
	}

	for name, test := range tests {
		test := test
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			req, err := http.NewRequest(test.method, fmt.Sprintf("http://%s%s", Addr, test.url), nil)
			if err != nil {
				t.Fatalf("could not create request: %s", err)
			}
			res, err := http.DefaultClient.Do(req)

			if err != nil {
				t.Fatalf("could not send request: %s", err)
			}

			if res.StatusCode != test.statusCode {
				t.Error("could not match status code")
				t.Errorf("got: %d", res.StatusCode)
				t.Errorf("want: %d", test.statusCode)
			}

			resBody, err := ioutil.ReadAll(res.Body)
			if err != nil {
				t.Fatalf("could not read response body: %s", err)
			}

			if !bytes.Equal(resBody, test.body) {
				t.Error("could not match status code")
				t.Errorf("got: %d", resBody)
				t.Errorf("want: %d", test.body)
			}
		})
	}
}

How to write tests

integration-test (server side)

$ go test ./server/... -v
=== RUN   TestStart
=== RUN   TestStart/not_found
=== PAUSE TestStart/not_found
=== RUN   TestStart/health
=== PAUSE TestStart/health
=== RUN   TestStart/users
=== PAUSE TestStart/users
=== CONT  TestStart/not_found
=== CONT  TestStart/health
=== CONT  TestStart/users
--- PASS: TestStart (0.00s)
    --- PASS: TestStart/health (0.50s)
    --- PASS: TestStart/not_found (0.50s)
    --- PASS: TestStart/users (0.50s)
PASS
ok      github.com/damianopetrungaro/presentation/server        0.807s

How to write tests

integration-test (server side)

package server

import (
	"bytes"
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func TestStart(t *testing.T) {
	ctx, cancelFn := context.WithCancel(context.Background())
	ch := make(chan struct{})

	go func() {
		Start(ctx, ch, log.New(ioutil.Discard, "", log.LstdFlags))
	}()

	select {
	case <-time.Tick(time.Second):
		t.Fatal("could not mark the server as started")
	case <-ch:
	}

	t.Cleanup(func() {
		cancelFn()
		select {
		case <-time.Tick(time.Second):
			t.Error("could not mark the server as shut down")
		case <-ch:
		}
	})

	testHandlerHelper(t, func(method, url string) (*http.Response, error) {
		req, err := http.NewRequest(method, fmt.Sprintf("http://%s%s", Addr, url), nil)
		if err != nil {
			t.Fatalf("could not create request: %s", err)
		}
		return http.DefaultClient.Do(req)
	})
}

func TestServer(t *testing.T) {
	srv := New()
	testHandlerHelper(t, func(method, url string) (*http.Response, error) {
		rec := httptest.NewRecorder()
		srv.Handler.ServeHTTP(rec, httptest.NewRequest(method, url, nil))
		return rec.Result(), nil
	})
}

func testHandlerHelper(t *testing.T, helper func(method, url string) (*http.Response, error)) {
	tests := map[string]struct {
		method     string
		url        string
		statusCode int
		body       []byte
	}{
		"health": {
			method:     http.MethodGet,
			url:        "/health",
			statusCode: http.StatusOK,
			body:       []byte("ok"),
		},
		"users": {
			method:     http.MethodGet,
			url:        "/users",
			statusCode: http.StatusOK,
			body:       []byte(`[{"id":1, "name": "Mario"}, {"id":2, "name": "Luigi"}]`),
		},
		"not found": {
			method:     http.MethodGet,
			url:        "/none",
			statusCode: http.StatusNotFound,
			body:       nil,
		},
	}

	for name, test := range tests {
		test := test
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			res, err := helper(test.method, test.url)
			if err != nil {
				t.Fatalf("could not send request: %s", err)
			}

			if res.StatusCode != test.statusCode {
				t.Error("could not match status code")
				t.Errorf("got: %d", res.StatusCode)
				t.Errorf("want: %d", test.statusCode)
			}

			resBody, err := ioutil.ReadAll(res.Body)
			if err != nil {
				t.Fatalf("could not read response body: %s", err)
			}

			if !bytes.Equal(resBody, test.body) {
				t.Error("could not match status code")
				t.Errorf("got: %d", resBody)
				t.Errorf("want: %d", test.body)
			}
		})
	}
}

How to write tests

integration-test (server side)

$ go test ./server/... -v
=== RUN   TestStart
=== RUN   TestStart/not_found
=== PAUSE TestStart/not_found
=== RUN   TestStart/health
=== PAUSE TestStart/health
=== RUN   TestStart/users
=== PAUSE TestStart/users
=== CONT  TestStart/not_found
=== CONT  TestStart/users
=== CONT  TestStart/health
--- PASS: TestStart (0.00s)
    --- PASS: TestStart/users (0.50s)
    --- PASS: TestStart/health (0.50s)
    --- PASS: TestStart/not_found (0.50s)
=== RUN   TestServer
=== RUN   TestServer/health
=== PAUSE TestServer/health
=== RUN   TestServer/users
=== PAUSE TestServer/users
=== RUN   TestServer/not_found
=== PAUSE TestServer/not_found
=== CONT  TestServer/health
=== CONT  TestServer/not_found
=== CONT  TestServer/users
--- PASS: TestServer (0.00s)
    --- PASS: TestServer/not_found (0.50s)
    --- PASS: TestServer/users (0.50s)
    --- PASS: TestServer/health (0.50s)
PASS
ok      github.com/damianopetrungaro/presentation/server        1.097s

How to write tests

integration-test

client side

How to write tests

package client

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
)

var (
	ErrCouldNotGetUser  = errors.New("could not get user")
	ErrCouldNotFindUser = errors.New("could not find user")
)

type (
	Client struct {
		c       *http.Client
		baseURL string
	}

	User struct {
		ID   string
		Name string
	}
)

func New(c *http.Client, url string) Client {
	return Client{
		c:       c,
		baseURL: url,
	}
}

func (c Client) Get(id string) (User, error) {
	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/users/%s", c.baseURL, id), nil)
	if err != nil {
		return User{}, fmt.Errorf("%w: could not create request: %s", ErrCouldNotGetUser, err)
	}

	res, err := c.c.Do(req)
	if err != nil {
		return User{}, fmt.Errorf("%w: could not do request: %s", ErrCouldNotGetUser, err)
	}

	switch {
	case res.StatusCode == http.StatusNotFound:
		return User{}, ErrCouldNotFindUser
	case res.StatusCode != http.StatusOK:
		return User{}, fmt.Errorf("%w: could not handle status code: %s", ErrCouldNotGetUser, err)
	}

	defer res.Body.Close()
	var u User
	if err := json.NewDecoder(res.Body).Decode(&u); err != nil {
		return User{}, fmt.Errorf("%w: could not decode response body: %s", ErrCouldNotGetUser, err)
	}

	return u, nil
}

integration-test (client side)

How to write tests

integration-test (client side)

package client

import (
	"errors"
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestClient(t *testing.T) {
	srv := server(t)

	srv.Start()
	t.Cleanup(func() {
		srv.Close()
	})

	c := New(http.DefaultClient, srv.URL)

	tests := map[string]struct {
		id   string
		err  error
		user User
	}{
		"existing user": {
			id:  "mario",
			err: nil,
			user: User{
				ID:   "mario",
				Name: "super mario",
			},
		},
		"not found": {
			id:   "luigi",
			err:  ErrCouldNotFindUser,
			user: User{},
		},
		"server error": {
			id:   "peach",
			err:  ErrCouldNotGetUser,
			user: User{},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			t.Parallel()

			u, err := c.Get(test.id)
			if !errors.Is(err, test.err) {
				t.Error("could not match error")
				t.Errorf("got: %s", err)
				t.Errorf("want: %s", test.err)
			}

			if test.user != u {
				t.Error("could not match user")
				t.Errorf("got: %v", u)
				t.Errorf("want: %v", test.user)
			}
		})
	}
}

func server(t *testing.T) *httptest.Server {
	t.Helper()

	return httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.URL.Path == "/users/mario" {
			if _, err := fmt.Fprint(w, `{"id":"mario", "name": "super mario"}`); err != nil {
				w.WriteHeader(http.StatusInternalServerError)
			}
			return
		}

		if r.URL.Path == "/users/luigi" {
			w.WriteHeader(http.StatusNotFound)
			return
		}

		if r.URL.Path == "/users/peach" {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
	}))
}

How to write tests

integration-test (client side)

$ go test ./client/... -v
=== RUN   TestClient
=== RUN   TestClient/server_error
=== PAUSE TestClient/server_error
=== RUN   TestClient/existing_user
=== PAUSE TestClient/existing_user
=== RUN   TestClient/not_found
=== PAUSE TestClient/not_found
=== CONT  TestClient/server_error
=== CONT  TestClient/existing_user
=== CONT  TestClient/not_found
--- PASS: TestClient (0.00s)
    --- PASS: TestClient/not_found (0.00s)
    --- PASS: TestClient/server_error (0.00s)
    --- PASS: TestClient/existing_user (0.00s)
PASS
ok      github.com/damianopetrungaro/presentation/client        0.095s

How to write tests

Interface-based

How to write tests

Interface-based = mock-based

How to write tests

DO NOT MOCK

From the stdlib the mock word appears only 14 times...

In 2 tests

if you can

How to write tests

package _interface

import (
	"context"
	"errors"
)

var ErrCouldNotFind = errors.New("...")
var ErrCouldNotAdd = errors.New("...")

type User struct {
	ID   string
	Name string
}

type Repo interface {
	Add(context.Context, User) error
	Get(context.Context, string) (User, error)
}

How to write tests

package _interface

import (
	"context"
	"database/sql"
	"fmt"
)

type SQLRepo struct {
	Conn *sql.DB
}

func (r *SQLRepo) Add(ctx context.Context, u User) error {
	_, err := r.Conn.ExecContext(ctx, "INSERT INTO `Users` (`id`, `name`) VALUES (?, ?)", u.ID, u.Name)
	if err != nil {
		return fmt.Errorf("%w: %s", ErrCouldNotAdd, err)
	}

	return nil
}

func (r *SQLRepo) Get(ctx context.Context, id string) (User, error) {
	var name string
	err := r.Conn.QueryRowContext(ctx, "SELECT name FROM Users WHERE id = ?", id).Scan(&name)
	if err != nil {
		return User{}, fmt.Errorf("%w: %s", ErrCouldNotFind, err)
	}

	return User{
		ID:   id,
		Name: name,
	}, nil
}

How to write tests

in memory

integration

How to write tests

using short flag or build tag

How to write tests

as integration

package _interface

import (
	_ "github.com/go-sql-driver/mysql"
	"testing"
)

func TestSQLRepoWithShortFlag(t *testing.T) {
	if testing.Short() {
		t.Skip()
	}

	db := connHelper(t)

	r := &SQLRepo{Conn: db}
	t.Cleanup(func() {
		if _, err := db.Exec("TRUNCATE TABLE Users"); err != nil {
			t.Fatalf("could not truncate table after test run: %s", err)
		}
	})

	t.Run("getting_non_existing_user", func(t *testing.T) {
		ctx := context.Background()
		u, err := r.Get(ctx, "")
		if u != (User{}) {
			t.Error("could not match user")
			t.Errorf("got: %v", u)
			t.Errorf("want: %v", User{})
		}

		if !errors.Is(err, ErrCouldNotFind) {
			t.Error("could not match error")
			t.Errorf("got: %v", err)
			t.Errorf("want: %v", ErrCouldNotFind)
		}
	})

	t.Run("adding_then_getting_user", func(t *testing.T) {
		ctx := context.Background()
		u := User{
			ID:   "number_one",
			Name: "Mario",
		}

		if err := r.Add(ctx, u); err != nil {
			t.Error("could not match error")
			t.Errorf("got: %v", err)
			t.Errorf("want: %v", nil)
			t.FailNow()
		}

		u2, err := r.Get(ctx, u.ID)
		if u != u2 {
			t.Error("could not match user")
			t.Errorf("got: %v", u2)
			t.Errorf("want: %v", u)
		}

		if err != nil {
			t.Error("could not match error")
			t.Errorf("got: %v", err)
			t.Errorf("want: %v", nil)
		}
	})
}


func connHelper(t *testing.T) *sql.DB {
	t.Helper()

	db, err := sql.Open("mysql", "users:users@tcp(localhost:3306)/users")
	if err != nil {
		t.Fatalf("could not open connection to MySQL: %s", err)
	}

	return db
}

How to write tests

as integration

$ go test ./interface/... -v
=== RUN   TestSQLRepoWithShortFlag
=== RUN   TestSQLRepoWithShortFlag/getting_non_existing_user
=== RUN   TestSQLRepoWithShortFlag/adding_then_getting_user
--- PASS: TestSQLRepoWithShortFlag (0.07s)
    --- PASS: TestSQLRepoWithShortFlag/getting_non_existing_user (0.03s)
    --- PASS: TestSQLRepoWithShortFlag/adding_then_getting_user (0.01s)
PASS
ok      github.com/damianopetrungaro/presentation/interface     0.258s

$ go test ./interface/... -v -short
=== RUN   TestSQLRepoWithShortFlag
    sql_test.go:10: 
--- SKIP: TestSQLRepoWithShortFlag (0.00s)
PASS
ok      github.com/damianopetrungaro/presentation/interface     0.075s

How to write tests

//+build integrations

package _interface

import (
	_ "github.com/go-sql-driver/mysql"
	"testing"
)

func TestSQLRepoWithBuildTag(t *testing.T) {
	db := connHelper(t)

	r := &SQLRepo{Conn: db}
	t.Cleanup(func() {
		if _, err := db.Exec("TRUNCATE TABLE Users"); err != nil {
			t.Fatalf("could not truncate table after test run: %s", err)
		}
	})

	t.Run("getting_non_existing_user", func(t *testing.T) {
		ctx := context.Background()
		u, err := r.Get(ctx, "")
		if u != (User{}) {
			t.Error("could not match user")
			t.Errorf("got: %v", u)
			t.Errorf("want: %v", User{})
		}

		if !errors.Is(err, ErrCouldNotFind) {
			t.Error("could not match error")
			t.Errorf("got: %v", err)
			t.Errorf("want: %v", ErrCouldNotFind)
		}
	})

	t.Run("adding_then_getting_user", func(t *testing.T) {
		ctx := context.Background()
		u := User{
			ID:   "number_one",
			Name: "Mario",
		}

		if err := r.Add(ctx, u); err != nil {
			t.Error("could not match error")
			t.Errorf("got: %v", err)
			t.Errorf("want: %v", nil)
			t.FailNow()
		}

		u2, err := r.Get(ctx, u.ID)
		if u != u2 {
			t.Error("could not match user")
			t.Errorf("got: %v", u2)
			t.Errorf("want: %v", u)
		}

		if err != nil {
			t.Error("could not match error")
			t.Errorf("got: %v", err)
			t.Errorf("want: %v", nil)
		}
	})
}


func connHelper(t *testing.T) *sql.DB {
	t.Helper()

	db, err := sql.Open("mysql", "users:users@tcp(localhost:3306)/users")
	if err != nil {
		t.Fatalf("could not open connection to MySQL: %s", err)
	}

	return db
}

How to write tests

as integration

$ go test ./interface/... -v -tags=integrations
=== RUN   TestSQLRepoWithBuildTag
=== RUN   TestSQLRepoWithBuildTag/getting_non_existing_user
=== RUN   TestSQLRepoWithBuildTag/adding_then_getting_user
--- PASS: TestSQLRepoWithBuildTag (0.05s)
    --- PASS: TestSQLRepoWithBuildTag/getting_non_existing_user (0.01s)
    --- PASS: TestSQLRepoWithBuildTag/adding_then_getting_user (0.01s)
PASS
ok      github.com/damianopetrungaro/presentation/interface     0.253s

$ go test ./interface/... -v
testing: warning: no tests to run
PASS
ok      github.com/damianopetrungaro/presentation/interface     0.060s [no tests to run]

How to write tests

with in memory

package _interface

import (
	"context"
	"sync"
)

type MemoryRepo struct {
	sync.Mutex
	Us map[string]User
}

func (r *MemoryRepo) Add(_ context.Context, u User) error {
	r.Lock()
	defer r.Unlock()

	r.Us[u.ID] = u
	return nil
}

func (r *MemoryRepo) Get(_ context.Context, id string) (User, error) {
	r.Lock()
	defer r.Unlock()

	u, ok := r.Us[id]
	if !ok {
		return User{}, ErrCouldNotFind
	}

	return u, nil
}

How to write tests

with in memory

package _interface

import (
	"testing"
)

func TestMemoryRepo(t *testing.T) {
	r := &MemoryRepo{Us: map[string]User{}}

	t.Run("getting_non_existing_user", func(t *testing.T) {
		ctx := context.Background()
		u, err := r.Get(ctx, "")
		if u != (User{}) {
			t.Error("could not match user")
			t.Errorf("got: %v", u)
			t.Errorf("want: %v", User{})
		}

		if !errors.Is(err, ErrCouldNotFind) {
			t.Error("could not match error")
			t.Errorf("got: %v", err)
			t.Errorf("want: %v", ErrCouldNotFind)
		}
	})

	t.Run("adding_then_getting_user", func(t *testing.T) {
		ctx := context.Background()
		u := User{
			ID:   "number_one",
			Name: "Mario",
		}

		if err := r.Add(ctx, u); err != nil {
			t.Error("could not match error")
			t.Errorf("got: %v", err)
			t.Errorf("want: %v", nil)
			t.FailNow()
		}

		u2, err := r.Get(ctx, u.ID)
		if u != u2 {
			t.Error("could not match user")
			t.Errorf("got: %v", u2)
			t.Errorf("want: %v", u)
		}

		if err != nil {
			t.Error("could not match error")
			t.Errorf("got: %v", err)
			t.Errorf("want: %v", nil)
		}
	})
}

How to write tests

with in memory

$ go test ./interface/... -v
=== RUN   TestMemoryRepo
=== RUN   TestMemoryRepo/getting_non_existing_user
=== RUN   TestMemoryRepo/adding_then_getting_user
--- PASS: TestMemoryRepo (0.00s)
    --- PASS: TestMemoryRepo/getting_non_existing_user (0.00s)
    --- PASS: TestMemoryRepo/adding_then_getting_user (0.00s)
PASS
ok      github.com/damianopetrungaro/presentation/interface     0.098s

Test coverage

Test coverage

package strings

import (
	"fmt"
	"strings"
)

type Manipulate func(string) string

func WrapWithPipes(s string) string {
	return fmt.Sprintf("|%s|", s)
}

func WrapWithUnderscores(s string) string {
	return fmt.Sprintf("_%s_", s)
}

func WrapWithDashes(s string) string {
	return fmt.Sprintf("-%s-", s)
}

func RemoveDashes(s string) string {
	return strings.ReplaceAll(s, "-", "")
}
$  go test -cover ./strings/...
ok      github.com/damianopetrungaro/presentation/strings       0.330s  coverage: 100.0% of statements

$ go test -cover ./strings/... -v
=== RUN   TestStrings
=== RUN   TestStrings/RemoveDashes
=== RUN   TestStrings/WrapWithPipes
=== RUN   TestStrings/WrapWithUnderscores
=== RUN   TestStrings/WrapWithDashes
--- PASS: TestStrings (0.00s)
    --- PASS: TestStrings/RemoveDashes (0.00s)
    --- PASS: TestStrings/WrapWithPipes (0.00s)
    --- PASS: TestStrings/WrapWithUnderscores (0.00s)
    --- PASS: TestStrings/WrapWithDashes (0.00s)
PASS
coverage: 100.0% of statements
ok      github.com/damianopetrungaro/presentation/strings       0.123s  coverage: 100.0% of statements

Test coverage

package test

// ... code here
func builderTest(b *work.Builder, p *load.Package) (buildAction, runAction, printAction *work.Action, err error) {
	// ... code here
	var cover *load.TestCover
	if testCover {
		cover = &load.TestCover{
			Mode:     testCoverMode,
			Local:    testCover && testCoverPaths == nil,
			Pkgs:     testCoverPkgs,
			Paths:    testCoverPaths,
			DeclVars: declareCoverVars,
		}
	}
	// ... code here
}

"go test -cover" instrument the binary

Test coverage

package strings

import (
	"fmt"
	"strings"
)

type Manipulate func(string) string

func WrapWithPipes(s string) string {
	GoCover.Count[0] = 1 // added automagically
	return fmt.Sprintf("|%s|", s)
}

func WrapWithUnderscores(s string) string {
	GoCover.Count[1] = 1 // added automagically
	return fmt.Sprintf("_%s_", s)
}

func WrapWithDashes(s string) string {
	GoCover.Count[2] = 1 // added automagically
	return fmt.Sprintf("-%s-", s)
}

func RemoveDashes(s string) string {
	GoCover.Count[3] = 1 // added automagically
	return strings.ReplaceAll(s, "-", "")
}

"go test -cover" instrument the binary

Test coverage

Test coverage

$ go test ./strings/... -coverprofile=coverage.out
ok      github.com/damianopetrungaro/presentation/strings       0.330s  coverage: 100.0% of statements

$ go tool cover -html=coverage.out

Test coverage

Detect data race

Detect data race

package _interface

import (
	"context"
)

type MemoryRepo struct {
	Us map[string]User
}

func (r *MemoryRepo) Add(_ context.Context, u User) error {
	r.Us[u.ID] = u
	return nil
}

func (r *MemoryRepo) Get(_ context.Context, id string) (User, error) {
	u, ok := r.Us[id]
	if !ok {
		return User{}, ErrCouldNotFind
	}

	return u, nil
}

Detect data race

$ go test -race -run Memory ./interface -v
=== RUN   TestMemoryRepo
=== RUN   TestMemoryRepo/getting_non_existing_user
=== PAUSE TestMemoryRepo/getting_non_existing_user
=== RUN   TestMemoryRepo/adding_then_getting_user
=== PAUSE TestMemoryRepo/adding_then_getting_user
=== CONT  TestMemoryRepo/adding_then_getting_user
=== CONT  TestMemoryRepo/getting_non_existing_user
==================
WARNING: DATA RACE
Read at 0x00c000116870 by goroutine 8:
  runtime.mapaccess2_faststr()
      /usr/local/go/src/runtime/map_faststr.go:107 +0x0
  github.com/damianopetrungaro/presentation/interface.(*MemoryRepo).Get()
      /Users/damianopetrungaro/go/src/github.com/damianopetrungaro/presentation/interface/memory.go:17 +0x75
  github.com/damianopetrungaro/presentation/interface.testRepo.func1()
      /Users/damianopetrungaro/go/src/github.com/damianopetrungaro/presentation/interface/interface_test.go:15 +0xdd
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1108 +0x202

Previous write at 0x00c000116870 by goroutine 9:
  runtime.mapassign_faststr()
      /usr/local/go/src/runtime/map_faststr.go:202 +0x0
  github.com/damianopetrungaro/presentation/interface.(*MemoryRepo).Add()
      /Users/damianopetrungaro/go/src/github.com/damianopetrungaro/presentation/interface/memory.go:12 +0x6f
  github.com/damianopetrungaro/presentation/interface.testRepo.func2()
      /Users/damianopetrungaro/go/src/github.com/damianopetrungaro/presentation/interface/interface_test.go:37 +0xea
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1108 +0x202

Goroutine 8 (running) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:1159 +0x796
  github.com/damianopetrungaro/presentation/interface.testRepo()
      /Users/damianopetrungaro/go/src/github.com/damianopetrungaro/presentation/interface/interface_test.go:12 +0xb9
  github.com/damianopetrungaro/presentation/interface.TestMemoryRepo()
      /Users/damianopetrungaro/go/src/github.com/damianopetrungaro/presentation/interface/memory_test.go:9 +0x99
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1108 +0x202

Goroutine 9 (running) created at:
  testing.(*T).Run()
      /usr/local/go/src/testing/testing.go:1159 +0x796
  github.com/damianopetrungaro/presentation/interface.testRepo()
      /Users/damianopetrungaro/go/src/github.com/damianopetrungaro/presentation/interface/interface_test.go:29 +0x144
  github.com/damianopetrungaro/presentation/interface.TestMemoryRepo()
      /Users/damianopetrungaro/go/src/github.com/damianopetrungaro/presentation/interface/memory_test.go:9 +0x99
  testing.tRunner()
      /usr/local/go/src/testing/testing.go:1108 +0x202
==================
=== CONT  TestMemoryRepo/getting_non_existing_user
    testing.go:1023: race detected during execution of test
--- FAIL: TestMemoryRepo (0.00s)
    --- PASS: TestMemoryRepo/adding_then_getting_user (0.00s)
    --- FAIL: TestMemoryRepo/getting_non_existing_user (0.00s)
=== CONT  
    testing.go:1023: race detected during execution of test
FAIL
FAIL    github.com/damianopetrungaro/presentation/interface     0.110s
FAIL

Detect data race

package _interface

import (
	"context"
	"sync"
)

type MemoryRepo struct {
	sync.Mutex
	Us map[string]User
}

func (r *MemoryRepo) Add(_ context.Context, u User) error {
	r.Lock()
	defer r.Unlock()

	r.Us[u.ID] = u
	return nil
}

func (r *MemoryRepo) Get(_ context.Context, id string) (User, error) {
	r.Lock()
	defer r.Unlock()

	u, ok := r.Us[id]
	if !ok {
		return User{}, ErrCouldNotFind
	}

	return u, nil
}

Detect data race

$ go test -race -run Memory ./interface -v
=== RUN   TestMemoryRepo
=== RUN   TestMemoryRepo/getting_non_existing_user
=== PAUSE TestMemoryRepo/getting_non_existing_user
=== RUN   TestMemoryRepo/adding_then_getting_user
=== PAUSE TestMemoryRepo/adding_then_getting_user
=== CONT  TestMemoryRepo/adding_then_getting_user
=== CONT  TestMemoryRepo/getting_non_existing_user
--- PASS: TestMemoryRepo (0.00s)
    --- PASS: TestMemoryRepo/adding_then_getting_user (0.00s)
    --- PASS: TestMemoryRepo/getting_non_existing_user (0.00s)
PASS
ok      github.com/damianopetrungaro/presentation/interface     0.120s

Enjoy your tests

$ go test -cover -race -count=5 ./...      
ok      github.com/damianopetrungaro/presentation/client        0.273s  coverage: 100.0% of statements
ok      github.com/damianopetrungaro/presentation/interface     0.302s  coverage: 100.0% of statements
ok      github.com/damianopetrungaro/presentation/server        0.186s  coverage: 100.0% of statements
ok      github.com/damianopetrungaro/presentation/strings       0.127s  coverage: 100.0% of statements

twitter: @damiano_dev
email: damianopetrungaro@gmail.com

Testing in Go

By Damiano Petrungaro

Testing in Go

Internals and best practices of the testing package regarding test

  • 918