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.tivolitouring.com
source: www.romaest.org
source:www.confinelive.it
source: www.greenparkmadama.it
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
- 1,061