q What is a t *testing.T?

Go has a built-in testing package for unit testing.

Rules

  • Test file should be named like xxx_test.go
  • Test function must start with Test.
  • Test function takes one argument only t *testing.T (import "testing")

Test example

// hello.go
package main
 
import "fmt"
 
func Hello() string {
	return "Hello, world"
}
 
func main() {
	fmt.Println(Hello())
}
 
// hello_test.go
package main
 
import "testing"
 
func TestHello(t *testing.T) {
	got := Hello()
	want := "Hello, world"
 
	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

Test function takes a pointer as a parameter of testing.T type, e.g. func TestHelloName(t *testing.T). It’s used for reporting and logging from the test.

A test ends when its Test function returns or calls any of the methods FailNow, Fatal, Fatalf, SkipNow, Skip, or Skipf. Those methods, as well as the Parallel method, must be called only from the goroutine running the Test function.

To run test call the go test CLI command. Example output:

➜ go test
PASS
ok      example.com/greetings   0.002s

➜ go test -v
=== RUN   TestHelloName
--- PASS: TestHelloName (0.00s)
=== RUN   TestHelloEmpty
--- PASS: TestHelloEmpty (0.00s)
PASS
ok      example.com/greetings   0.002s

Subtests

Tests can be grouped in the function like this:

func TestHello(t *testing.T) {
	t.Run("saying hello to people", func(t *testing.T) {
		got := Hello("Chris")
		want := "Hello, Chris"
 
		if got != want {
			t.Errorf("got %q want %q", got, want)
		}
	})
	t.Run("say 'Hello, World' when an empty string is supplied", func(t *testing.T) {
		got := Hello("")
		want := "Hello, World"
 
		if got != want {
			t.Errorf("got %q want %q", got, want)
		}
	})
}

You can set up shared code that can be used in multiple tests.

Helper functions

func TestHello(t *testing.T) {
	t.Run("saying hello to people", func(t *testing.T) {
		got := Hello("Chris")
		want := "Hello, Chris"
		assertCorrectMessage(t, got, want)
	})
 
	t.Run("empty string defaults to 'world'", func(t *testing.T) {
		got := Hello("")
		want := "Hello, World"
		assertCorrectMessage(t, got, want)
	})
 
}
 
func assertCorrectMessage(t testing.TB, got, want string) {
	t.Helper()
	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

Note that in the helper function t testing.TB used. It’s an interface that satisfies T (Test) and B (Benchmark).

t.Helper() is needed to tell the test suite that this method is a helper. By doing this when it fails the line number reported will be in our function call rather than inside our test helper.

Benchmarking

Coverage

go test -cover

Example output:

➜ go test -cover
PASS
coverage: 100.0% of statements
ok      example.com/iteration   0.002s

Test structure

I noticed in Go there’s a convention on how the tests should be structured. You have got and want variables you compare at the end. In fail message you format string with the structure “expected X, got Y”.

func TestSplit(t *testing.T) {
    got := Split("a/b/c", "/")
    want := []string{"a", "b", "c"}
    if !reflect.DeepEqual(want, got) {
         t.Fatalf("expected: %v, got: %v", want, got)
    }
}

Table-driven tests

func TestSplit(t *testing.T) {  
    tests := []struct {  
        input string  
        sep   string  
        want  []string  
    }{  
        {input: "a/b/c", sep: "/", want: []string{"a", "b", "c"}},  
        {input: "a/b/c", sep: ",", want: []string{"a/b/c"}},  
        {input: "abc", sep: "/", want: []string{"abc"}},  
    }   
  
    for _, tc := range tests {  
        got := Split(tc.input, tc.sep)  
        if !reflect.DeepEqual(tc.want, got) {  
            t.Fatalf("expected: %v, got: %v", tc.want, got)  
        }  
    }  
}

References