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

  1. Make it easy to add new test cases.
  2. Use test coverage to find untested code.
  3. Coverage is no substitute for thought.
  4. Write exhaustive tests.
  5. Separate test cases from test logic.
  6. Look for special cases.
  7. If you didn’t add a test, you didn’t fix the bug.
  8. Not everything fits in a table.
  9. Test cases can be in testdata files.
  10. Compare against other implementations.
  11. Make test failures readable.
  12. If the answer can change, write code to update them.
  13. Use txtar for multi-file test cases.
  14. Annotate existing formats to create testing mini-languages.
  15. Write parsers and printers to simplify tests.
  16. Code quality is limited by test quality.
  17. Scripts make good tests.
  18. Try rsc.io/script for your own script-based test cases.
  19. Improve your tests over time.
  20. 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.

#Reading