Testing in GO

When the creators of golang were developing the architecture of the language they were caring out about developers which will use it in real work. One of the most important processes of development it's the testing, and they did it very convenient and easy to use as they use it themselves.

To create the test enough to declare ".go" file with "_test" postfix in the name. This file will be ignored by the compiler when you will assemble your application. When you run the test every module in the project «go» compile the module with "_test.go" files as independent application and run the test cases one by one.

Let's make simple GO test

First of all, we need what exactly will be to test.

// fact.go
package fact

func Fact(v int) int {
	switch {
	case v < 2:
		return 1
	case v == 2:
		return 2
	}
	return v * Fact(v-1)
}


And test file at the same module

// fact_test.go
package fact

import (
	"fmt"
	"testing"
)

func TestFact(t *testing.T) {
	var tests = [][2]int{
		{0, 1}, {1, 1}, {2, 2}, {3, 6}, {4, 24}, {5, 120},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("fact_%d", test[0]), func(t *testing.T) {
			if fact := Fact(test[0]); fact != test[1] {
				t.Errorf("factorial from %d must be %d not %d", test[0], test[1], fact)
			}
		})
	}
}


Package “testing” it’s the standard package from Golang. For to split the complex test as in our case you can run the subtests which help to find the problem in the test.

go test -timeout 30s -run ^TestFact$

--- FAIL: TestFact (0.00s)
    --- FAIL: TestFact/fact_0 (0.00s)
    	fact_test.go:16: factorial from 0 must be 1 not 3
    --- FAIL: TestFact/fact_1 (0.00s)
    	fact_test.go:16: factorial from 1 must be 1 not 3
    --- FAIL: TestFact/fact_2 (0.00s)
    	fact_test.go:16: factorial from 2 must be 2 not 6
    --- FAIL: TestFact/fact_3 (0.00s)
    	fact_test.go:16: factorial from 3 must be 6 not 18
    --- FAIL: TestFact/fact_4 (0.00s)
    	fact_test.go:16: factorial from 4 must be 24 not 72
    --- FAIL: TestFact/fact_5 (0.00s)
    	fact_test.go:16: factorial from 5 must be 120 not 360
FAIL
exit status 1
FAIL	_/workspace/gotests/fact	0.006s
Error: Tests failed.


In the “testing” module you can find the logging functions but no assert, unfortunately. If you like the assert approach you can use “testify” project.

Benchmarks

Tests are good but something is missing. In highroad, the speed is highly important here creators of go gave us the benchmark tool. Let's make the benchmark test.

func BenchmarkFact(b *testing.B) {
	var numbers = []int{1, 27, 322, 43, 51, 103, 221, 342, 454, 5635, 8835, 1232, 4322, 7892, 1292}

	count := len(numbers)
	for i := 0; i < 10000; i++ {
		_ = Fact(numbers[i%count])
	}
}


go test -benchmem -run=^$ -bench ^BenchmarkFact$

goos: darwin
goarch: amd64
BenchmarkFact-8   	2000000000	         0.05 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	_/workspace/gotests/fact	0.890s
Success: Benchmarks passed.


0.05 ns/op looks good, but can we make it faster? In our *Fact* function we have a potential problem with the call stack because it’s recursive. Let’s test it with number 10000000. We getting the runtime: goroutine stack exceeds 1000000000-byte limit error.

Here is non-recursive implementation.

func Fact2(v int) (r int) {
	switch {
	case v < 2:
		return 1
	case v == 2:
		return 2
	}

	r = v
	for v--; v > 1; v-- {
		r *= v
	}
	return
}


Change the test and benchmark.

func TestFact(t *testing.T) {
	var tests = [][2]int{
		{0, 1}, {1, 1}, {2, 2}, {3, 6}, {4, 24}, {5, 120},
	}

	for _, test := range tests {
		t.Run(fmt.Sprintf("fact_%d", test[0]), func(t *testing.T) {
			if fact := Fact(test[0]); fact != test[1] {
				t.Errorf("factorial from %d must be %d not %d", test[0], test[1], fact)
			}
		})

		t.Run(fmt.Sprintf("fact2_%d", test[0]), func(t *testing.T) {
			if fact := Fact2(test[0]); fact != test[1] {
				t.Errorf("factorial2 from %d must be %d not %d", test[0], test[1], fact)
			}
		})
	}
}


Benchmark.

func BenchmarkFact(b *testing.B) {
	var numbers = []int{1, 27, 322, 43, 51, 103, 221, 342, 454, 5635, 8835, 1232, 4322, 7892, 1292}
	b.Run("fact1", benchFact(numbers, Fact))
	b.Run("fact2", benchFact(numbers, Fact2))
}

func benchFact(numbers []int, factFn func(int) int) func(*testing.B) {
	return func(b *testing.B) {
		count := len(numbers)
		for i := 0; i < 10000; i++ {
			_ = factFn(numbers[i%count])
		}
	}
}


So what we have in the result.

go test -benchmem -run=^$ -bench ^BenchmarkFact$

goos: darwin
goarch: amd64
BenchmarkFact/fact1-8         	2000000000	         0.05 ns/op	       0 B/op	       0 allocs/op
BenchmarkFact/fact2-8         	2000000000	         0.01 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	_/workspace/gotests/fact	0.989s
Success: Benchmarks passed.


Now it works faster, so we are succeeding.

Conclusion

The go have almost everything what you need for productive work and creating the tests of any complexity. If something you don't have from the basic library you always can find it from some other vendors. Of course, if we talking about the server-side development.

Links


  1. testing — The Go Programming Language
  2. testify: A toolkit with common assertions and mocks that plays nicely with the standard library

0 comments

Only registered users can comment.