Advanced Testing Topics
#Advanced Testing with Go
Advanced Testing with Go by Michelle Hashimoto - video, slides,
code
Notes:
Testing with golden files and updating them (go test -update to save in testdata).
// - advanced/advanced-testing/golden_test.go -
package testing
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
var update = flag.Bool("update", false, "update golden files")
type Person struct {
Name string `json:"name,omitempty"`
}
func (p Person) Export() ([]byte, error) {
return json.Marshal(p)
}
func TestPerson_Export(t *testing.T) {
var (
fixture = func(name string) (location string) {
t.Helper()
return filepath.Join("testdata", fmt.Sprintf("%s.json", name))
}
tests = map[string]struct {
s Person
want []byte
assertion assert.ErrorAssertionFunc
}{
"Joe": {
s: Person{Name: "Joe"},
want: readFile(fixture("Joe")),
assertion: assert.NoError,
},
}
)
for name, tt := range tests {
tt := tt
name := name
t.Run(name, func(t *testing.T) {
t.Parallel()
got, err := tt.s.Export()
tt.assertion(t, err)
assert.Equal(t, tt.want, got)
if *update {
_ = writeFile(fixture("Joe"), bytes.NewBuffer(got))
}
})
}
}
Having testing.go or testing_.go (or file_testing.go) to provide Testing API.
// - advanced/advanced-testing/testing.go -
package testing
import (
"bytes"
"io"
"os"
)
// Error checking omitted for brevity
func readFile(location string) []byte {
var buf bytes.Buffer
f, err := os.Open(location)
if err != nil {
return nil
}
defer f.Close()
if _, err := io.Copy(&buf, f); err != nil {
return nil
}
return buf.Bytes()
}
func writeFile(location string, r io.Reader) error {
f, err := os.OpenFile(location, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
if _, err := io.Copy(f, r); err != nil {
return err
}
return nil
}
network_testing.go server listner fixture (see https://github.com/hashicorp/go-plugin ).
// - advanced/advanced-testing/network_testing.go -
package testing
import (
"net"
"net/rpc"
"testing"
)
func TestConn(t testing.T) (net.Conn, net.Conn) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("err: %s", err)
}
// Start a goroutine to accept our client connection
var serverConn net.Conn
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
defer l.Close()
var err error
serverConn, err = l.Accept()
if err != nil {
t.Fatalf("err: %s", err)
}
}()
// Connect to the server
clientConn, err := net.Dial("tcp", l.Addr().String())
if err != nil {
t.Fatalf("err: %s", err)
}
// Wait for the server side to acknowledge it has connected
<-doneCh
return clientConn, serverConn
}
// TestRPCConn returns a rpc client and server connected to each other.
func TestRPCConn(t testing.T) (*rpc.Client, *rpc.Server) {
t.Helper()
clientConn, serverConn := TestConn(t)
server := rpc.NewServer()
go server.ServeConn(serverConn)
client := rpc.NewClient(clientConn)
return client, server
}
subprocessing_test.go testing subprocesses with Entrypoint
// - advanced/advanced-testing/subprocessing_test.go -
package testing
import (
"bytes"
"fmt"
"os"
"os/exec"
"strings"
"testing"
)
var testHasGit bool
func init() {
if _, err := exec.LookPath("git"); err == nil {
testHasGit = true
}
}
func TestGitGetter(t *testing.T) {
if !testHasGit {
t.Log("git not found, skipping")
t.Skip()
}
}
func helperProcess(s ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--"}
cs = append(cs, s...)
env := []string{
"GO_WANT_HELPER_PROCESS=1",
}
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = append(env, os.Environ()...)
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return cmd
}
func TestHelperProcess(*testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
// Find the arguments to our helper, which are the arguments past
// the "--" in the command line.
args := os.Args
for len(args) > 0 {
if args[0] == "--" {
args = args[1:]
break
}
args = args[1:]
}
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "No command\n")
os.Exit(2)
}
cmd, args := args[0], args[1:]
switch cmd {
case "test":
fmt.Fprint(os.Stdout, "foo")
fmt.Fprint(os.Stderr, "bar")
os.Exit(0)
default:
fmt.Fprintf(os.Stderr, "unknown command called [%s] with arguments [%v]", cmd, args)
os.Exit(0)
}
}
func TestCmdFoo_Args_Bar(t *testing.T) {
var stdout, stderr bytes.Buffer
p := helperProcess("foo", "-bar")
p.Stdout = &stdout
p.Stderr = &stderr
if err := p.Run(); err != nil {
t.Errorf("err: %s", err)
}
if !strings.Contains(stderr.String(), "unknown command") {
t.Error(stdout.String())
}
}
#Testing Techniques
Testing Techniques by by Andrew Gerrand: video slides
#Go Testing By Example
Go Testing By Example by Russ Cox
- Make it easy to add new test cases.
- Use test coverage to find untested code.
- Coverage is no substitute for thought.
- Write exhaustive tests.
- Separate test cases from test logic.
- Look for special cases.
- If you didn’t add a test, you didn’t fix the bug.
- Not everything fits in a table.
- Test cases can be in testdata files.
- Compare against other implementations.
- Make test failures readable.
- If the answer can change, write code to update them.
- Use txtar for multi-file test cases.
- Annotate existing formats to create testing mini-languages.
- Write parsers and printers to simplify tests.
- Code quality is limited by test quality.
- Scripts make good tests.
- Try rsc.io/script for your own script-based test cases.
- Improve your tests over time.
- Aim for continuous deployment.
#Testing Patterns
A test is not a unit test if:
- It talks to the database
- It communicates across the network
- It touches the file system
- It can’t run at the same time as any of your other unit tests
- You have to do special things to your environment to run it.