| 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 http://golang.org/pkg/testing/ |
| |
| .link http://golang.org/cmd/go/ |
| |
| .link http://golang.org |
| |