| Testing Techniques | |
| Google I/O 2014 | |
| Andrew Gerrand | |
| adg@golang.org | |
| * Video | |
| This talk was presented at golang-syd in July 2014. | |
| .link http://www.youtube.com/watch?v=ndmB0bj7eyw Watch the talk on YouTube | |
| * The basics | |
| * Testing Go code | |
| Go has a built-in testing framework. | |
| It is provided by the `testing` package and the `go` `test` command. | |
| Here is a complete test file that tests the `strings.Index` function: | |
| .code testing/test1/string_test.go | |
| * Table-driven tests | |
| Go's struct literal syntax makes it easy to write table-driven tests: | |
| .code testing/test2/string_test.go /func TestIndex/,/^}/ | |
| * T | |
| The `*testing.T` argument is used for error reporting: | |
| t.Errorf("got bar = %v, want %v", got, want) | |
| t.Fatalf("Frobnicate(%v) returned error: %v", arg, err) | |
| t.Logf("iteration %v", i) | |
| And enabling parallel tests: | |
| t.Parallel() | |
| And controlling whether a test runs at all: | |
| if runtime.GOARCH == "arm" { | |
| t.Skip("this doesn't work on ARM") | |
| } | |
| * Running tests | |
| The `go` `test` command runs tests for the specified package. | |
| (It defaults to the package in the current directory.) | |
| $ go test | |
| PASS | |
| $ go test -v | |
| === RUN TestIndex | |
| --- PASS: TestIndex (0.00 seconds) | |
| PASS | |
| To run the tests for all my projects: | |
| $ go test github.com/nf/... | |
| Or for the standard library: | |
| $ go test std | |
| * Test coverage | |
| The `go` tool can report test coverage statistics. | |
| $ go test -cover | |
| PASS | |
| coverage: 96.4% of statements | |
| ok strings 0.692s | |
| The `go` tool can generate coverage profiles that may be interpreted by the `cover` tool. | |
| $ go test -coverprofile=cover.out | |
| $ go tool cover -func=cover.out | |
| strings/reader.go: Len 66.7% | |
| strings/strings.go: TrimSuffix 100.0% | |
| ... many lines omitted ... | |
| strings/strings.go: Replace 100.0% | |
| strings/strings.go: EqualFold 100.0% | |
| total: (statements) 96.4% | |
| * Coverage visualization | |
| $ go tool cover -html=cover.out | |
| .image testing/cover.png | |
| * Advanced techniques | |
| * An example program | |
| *outyet* is a web server that announces whether or not a particular Go version has been tagged. | |
| go get github.com/golang/example/outyet | |
| .image testing/go1.1.png | |
| * Testing HTTP clients and servers | |
| The `net/http/httptest` package provides helpers for testing code that makes or serves HTTP requests. | |
| * httptest.Server | |
| An `httptest.Server` listens on a system-chosen port on the local loopback interface, for use in end-to-end HTTP tests. | |
| type Server struct { | |
| URL string // base URL of form http://ipaddr:port with no trailing slash | |
| Listener net.Listener | |
| // TLS is the optional TLS configuration, populated with a new config | |
| // after TLS is started. If set on an unstarted server before StartTLS | |
| // is called, existing fields are copied into the new config. | |
| TLS *tls.Config | |
| // Config may be changed after calling NewUnstartedServer and | |
| // before Start or StartTLS. | |
| Config *http.Server | |
| } | |
| func NewServer(handler http.Handler) *Server | |
| func (*Server) Close() error | |
| * httptest.Server in action | |
| This code sets up a temporary HTTP server that serves a simple "Hello" response. | |
| .play testing/httpserver.go /START/,/STOP/ | |
| * httptest.ResponseRecorder | |
| `httptest.ResponseRecorder` is an implementation of `http.ResponseWriter` that records its mutations for later inspection in tests. | |
| type ResponseRecorder struct { | |
| Code int // the HTTP response code from WriteHeader | |
| HeaderMap http.Header // the HTTP response headers | |
| Body *bytes.Buffer // if non-nil, the bytes.Buffer to append written data to | |
| Flushed bool | |
| } | |
| * httptest.ResponseRecorder in action | |
| By passing a `ResponseRecorder` into an HTTP handler we can inspect the generated response. | |
| .play testing/httprecorder.go /START/,/STOP/ | |
| * Race Detection | |
| A data race occurs when two goroutines access the same variable concurrently and at least one of the accesses is a write. | |
| To help diagnose such bugs, Go includes a built-in data race detector. | |
| Pass the `-race` flag to the go tool to enable the race detector: | |
| $ go test -race mypkg // to test the package | |
| $ go run -race mysrc.go // to run the source file | |
| $ go build -race mycmd // to build the command | |
| $ go install -race mypkg // to install the package | |
| * Testing with concurrency | |
| When testing concurrent code, there's a temptation to use sleep; | |
| it's easy and works most of the time. | |
| But "most of the time" isn't always and flaky tests result. | |
| We can use Go's concurrency primitives to make flaky sleep-driven tests reliable. | |
| * Finding errors with static analysis: vet | |
| The `vet` tool checks code for common programmer mistakes: | |
| - bad printf formats, | |
| - bad build tags, | |
| - bad range loop variable use in closures, | |
| - useless assignments, | |
| - unreachable code, | |
| - bad use of mutexes, | |
| - and more. | |
| Usage: | |
| go vet [package] | |
| * Testing from the inside | |
| Most tests are compiled as part of the package under test. | |
| This means they can access unexported details, as we have already seen. | |
| * Testing from the outside | |
| Occasionally you want to run your tests from outside the package under test. | |
| (Test files as `package` `foo_test` instead of `package` `foo`.) | |
| This can break dependency cycles. For example: | |
| - The `testing` package uses `fmt`. | |
| - The `fmt` tests must import `testing`. | |
| - So the `fmt` tests are in package `fmt_test` and can import both `testing` and `fmt`. | |
| * Mocks and fakes | |
| Go eschews mocks and fakes in favor of writing code that takes broad interfaces. | |
| For example, if you're writing a file format parser, don't write a function like this: | |
| func Parse(f *os.File) error | |
| instead, write functions that take the interface you need: | |
| func Parse(r io.Reader) error | |
| (An `*os.File` implements `io.Reader`, as does `bytes.Buffer` or `strings.Reader`.) | |
| * Subprocess tests | |
| Sometimes you need to test the behavior of a process, not just a function. | |
| .code testing/subprocess/subprocess.go /func Crasher/,/^}/ | |
| To test this code, we invoke the test binary itself as a subprocess: | |
| .code testing/subprocess/subprocess_test.go /func TestCrasher/,/^}/ | |
| * More information | |
| .link /pkg/testing/ go.dev/pkg/testing/ | |
| .link /cmd/go/ go.dev/cmd/go/ | |
| .link / go.dev | |