"Testing Techniques" talk
LGTM=gmlewis, r, campoy
R=dsymonds, crawshaw, gmlewis, r, sameer, bcmills, campoy
CC=golang-codereviews
https://golang.org/cl/108170043
diff --git a/2014/testing.slide b/2014/testing.slide
new file mode 100644
index 0000000..1f290cf
--- /dev/null
+++ b/2014/testing.slide
@@ -0,0 +1,259 @@
+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 intepreted 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
+
diff --git a/2014/testing/cover.png b/2014/testing/cover.png
new file mode 100644
index 0000000..57d5d67
--- /dev/null
+++ b/2014/testing/cover.png
Binary files differ
diff --git a/2014/testing/go1.1.png b/2014/testing/go1.1.png
new file mode 100644
index 0000000..beb3325
--- /dev/null
+++ b/2014/testing/go1.1.png
Binary files differ
diff --git a/2014/testing/httprecorder.go b/2014/testing/httprecorder.go
new file mode 100644
index 0000000..95ea793
--- /dev/null
+++ b/2014/testing/httprecorder.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "net/http/httptest"
+)
+
+func main() {
+ // START OMIT
+ handler := func(w http.ResponseWriter, r *http.Request) {
+ http.Error(w, "something failed", http.StatusInternalServerError)
+ }
+
+ req, err := http.NewRequest("GET", "http://example.com/foo", nil)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ w := httptest.NewRecorder()
+ handler(w, req)
+
+ fmt.Printf("%d - %s", w.Code, w.Body.String())
+ // STOP OMIT
+}
diff --git a/2014/testing/httpserver.go b/2014/testing/httpserver.go
new file mode 100644
index 0000000..5917705
--- /dev/null
+++ b/2014/testing/httpserver.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "net/http/httptest"
+)
+
+func main() {
+ // START OMIT
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintln(w, "Hello, client")
+ }))
+ defer ts.Close()
+
+ res, err := http.Get(ts.URL)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ greeting, err := ioutil.ReadAll(res.Body)
+ res.Body.Close()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("%s", greeting)
+ // STOP OMIT
+}
diff --git a/2014/testing/subprocess/subprocess.go b/2014/testing/subprocess/subprocess.go
new file mode 100644
index 0000000..8fb8fc0
--- /dev/null
+++ b/2014/testing/subprocess/subprocess.go
@@ -0,0 +1,11 @@
+package subprocess
+
+import (
+ "fmt"
+ "os"
+)
+
+func Crasher() {
+ fmt.Println("Going down in flames!")
+ os.Exit(1)
+}
diff --git a/2014/testing/subprocess/subprocess_test.go b/2014/testing/subprocess/subprocess_test.go
new file mode 100644
index 0000000..27b05d0
--- /dev/null
+++ b/2014/testing/subprocess/subprocess_test.go
@@ -0,0 +1,21 @@
+package subprocess
+
+import (
+ "os"
+ "os/exec"
+ "testing"
+)
+
+func TestCrasher(t *testing.T) {
+ if os.Getenv("BE_CRASHER") == "1" {
+ Crasher()
+ return
+ }
+ cmd := exec.Command(os.Args[0], "-test.run=TestCrasher")
+ cmd.Env = append(os.Environ(), "BE_CRASHER=1")
+ err := cmd.Run()
+ if e, ok := err.(*exec.ExitError); ok && !e.Success() {
+ return
+ }
+ t.Fatalf("process ran with err %v, want exit status 1", err)
+}
diff --git a/2014/testing/test1/string_test.go b/2014/testing/test1/string_test.go
new file mode 100644
index 0000000..c458ffe
--- /dev/null
+++ b/2014/testing/test1/string_test.go
@@ -0,0 +1,14 @@
+package strings_test
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestIndex(t *testing.T) {
+ const s, sep, want = "chicken", "ken", 4
+ got := strings.Index(s, sep)
+ if got != want {
+ t.Errorf("Index(%q,%q) = %v; want %v", s, sep, want, got)
+ }
+}
diff --git a/2014/testing/test2/string_test.go b/2014/testing/test2/string_test.go
new file mode 100644
index 0000000..886597a
--- /dev/null
+++ b/2014/testing/test2/string_test.go
@@ -0,0 +1,43 @@
+package strings_test
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+)
+
+func TestIndex(t *testing.T) {
+ var tests = []struct {
+ s string
+ sep string
+ out int
+ }{
+ {"", "", 0},
+ {"", "a", -1},
+ {"fo", "foo", -1},
+ {"foo", "foo", 0},
+ {"oofofoofooo", "f", 2},
+ // etc
+ }
+ for _, test := range tests {
+ actual := strings.Index(test.s, test.sep)
+ if actual != test.out {
+ t.Errorf("Index(%q,%q) = %v; want %v", test.s, test.sep, actual, test.out)
+ }
+ }
+}
+
+func BenchmarkIndex(b *testing.B) {
+ const s = "some_text=some☺value"
+ for i := 0; i < b.N; i++ {
+ strings.Index(s, "v")
+ }
+}
+
+func ExampleIndex() {
+ fmt.Println(strings.Index("chicken", "ken"))
+ fmt.Println(strings.Index("chicken", "dmr"))
+ // Output:
+ // 4
+ // -1
+}