How can I do test setup using the testing package in Go
Unit TestingGoUnit Testing Problem Overview
How can I do overall test setup processing which sets the stage for all the tests when using the testing package?
As an example in Nunit there is a [SetUp]
attribute.
[TestFixture]
public class SuccessTests
{
[SetUp] public void Init()
{ /* Load test data */ }
}
Unit Testing Solutions
Solution 1 - Unit Testing
Starting with Go 1.4 you can implement setup/teardown (no need to copy your functions before/after each test). The documentation is outlined here in the Main section:
> TestMain runs in the main goroutine and can do whatever setup and > teardown is necessary around a call to m.Run. It should then call > os.Exit with the result of m.Run
It took me some time to figure out that this means that if a test contains a function func TestMain(m *testing.M)
then this function will be called instead of running the test. And in this function I can define how the tests will run. For example I can implement global setup and teardown:
> func TestMain(m *testing.M) { > setup() > code := m.Run() > shutdown() > os.Exit(code) > }
A couple of other examples can be found here.
> The TestMain feature added to Go’s testing framework in the latest > release is a simple solution for several testing use cases. TestMain > provides a global hook to perform setup and shutdown, control the > testing environment, run different code in a child process, or check > for resources leaked by test code. Most packages will not need a > TestMain, but it is a welcome addition for those times when it is > needed.
Solution 2 - Unit Testing
This can be achieved by putting a init()
function in the myfile_test.go
file. This will be run before the init()
function.
// myfile_test.go
package main
func init() {
/* load test data */
}
The myfile_test.init() will be called before the package init() function.
Solution 3 - Unit Testing
Given a simple function to unit test:
package math
func Sum(a, b int) int {
return a + b
}
You can test it with a setup function that returns teardown function. And after calling setup() you can make a deferred call to teardown().
package math
import "testing"
func setupTestCase(t *testing.T) func(t *testing.T) {
t.Log("setup test case")
return func(t *testing.T) {
t.Log("teardown test case")
}
}
func setupSubTest(t *testing.T) func(t *testing.T) {
t.Log("setup sub test")
return func(t *testing.T) {
t.Log("teardown sub test")
}
}
func TestAddition(t *testing.T) {
cases := []struct {
name string
a int
b int
expected int
}{
{"add", 2, 2, 4},
{"minus", 0, -2, -2},
{"zero", 0, 0, 0},
}
teardownTestCase := setupTestCase(t)
defer teardownTestCase(t)
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
teardownSubTest := setupSubTest(t)
defer teardownSubTest(t)
result := Sum(tc.a, tc.b)
if result != tc.expected {
t.Fatalf("expected sum %v, but got %v", tc.expected, result)
}
})
}
}
Go testing tool will report the logging statements in the shell console:
% go test -v
=== RUN TestAddition
=== RUN TestAddition/add
=== RUN TestAddition/minus
=== RUN TestAddition/zero
--- PASS: TestAddition (0.00s)
math_test.go:6: setup test case
--- PASS: TestAddition/add (0.00s)
math_test.go:13: setup sub test
math_test.go:15: teardown sub test
--- PASS: TestAddition/minus (0.00s)
math_test.go:13: setup sub test
math_test.go:15: teardown sub test
--- PASS: TestAddition/zero (0.00s)
math_test.go:13: setup sub test
math_test.go:15: teardown sub test
math_test.go:8: teardown test case
PASS
ok github.com/kare/go-unit-test-setup-teardown 0.010s
%
You can pass some additional parameters to setup/teardown with this approach.
Solution 4 - Unit Testing
The Go testing framework doesn't have anything equivalent to NUnit's SetUp attribute (marking a function to be called before each test in the suite). There are a few options though:
-
Simply call your
SetUp
function from each test where it is needed. -
Use an extension to Go's testing framework that implements xUnit paradigms and concepts. Three strong options come to mind:
Each of these libraries encourage you to organize your tests into suites/fixtures similar to other xUnit frameworks, and will call the setup methods on the suite/fixture type before each of the Test*
methods.
Solution 5 - Unit Testing
Typically, tests in go aren't written in the same style as other languages. Often, there's relatively fewer test functions, but each contains a table-driven set of test cases. See this article written by one of the Go team.
With a table-driven test, you simply put any setup code before the loop that executes the individual test-cases specified in the table, and put any cleanup code afterwards.
If you still have shared setup code between test functions, you can extract the shared setup code into a function, and use a sync.Once
if it's important that it's executed exactly once (or as another answer suggests, use init()
, but this has the disadvantage that the setup will be done even if the test cases aren't run (perhaps because you've limited the test cases by using go test -run <regexp>
.)
I'd say if you think you need shared setup between different tests that gets executed exactly once you should have a think if you really need it, and if a table-driven test wouldn't be better.
Solution 6 - Unit Testing
With the following template, you can make a one line call in each TestMethod that does both setup and tear-down.
func setupTest() func() {
// Setup code here
// tear down later
return func() {
// tear-down code here
}
}
func TestMethod(t *testing.T) {
defer setupTest()()
// asserts, ensures, requires... here
}
Solution 7 - Unit Testing
In case someone is looking an alternative of @BeforeEach (which runs before each test in a test file) and @AfterEach (which runs after test in a test file), here's a helper snippet.
func CreateForEach(setUp func(), tearDown func()) func(func()) {
return func(testFunc func()) {
setUp()
testFunc()
tearDown()
}
}
You can use it like below with help of TestMain
var RunTest = CreateForEach(setUp, tearDown)
func setUp() {
// SETUP METHOD WHICH IS REQUIRED TO RUN FOR EACH TEST METHOD
// your code here
}
func tearDown() {
// TEAR DOWN METHOD WHICH IS REQUIRED TO RUN FOR EACH TEST METHOD
// your code here
}
fun TestSample(t *testing.T) {
RunTest(func() {
// YOUR CODE HERE
})
}
also you can check: go-beforeeach
Solution 8 - Unit Testing
Shameless plug, I created https://github.com/houqp/gtest to help solve exactly this problem.
Here is a quick example:
import (
"strings"
"testing"
"github.com/houqp/gtest"
)
type SampleTests struct{}
// Setup and Teardown are invoked per test group run
func (s *SampleTests) Setup(t *testing.T) {}
func (s *SampleTests) Teardown(t *testing.T) {}
// BeforeEach and AfterEach are invoked per test run
func (s *SampleTests) BeforeEach(t *testing.T) {}
func (s *SampleTests) AfterEach(t *testing.T) {}
func (s *SampleTests) SubTestCompare(t *testing.T) {
if 1 != 1 {
t.FailNow()
}
}
func (s *SampleTests) SubTestCheckPrefix(t *testing.T) {
if !strings.HasPrefix("abc", "ab") {
t.FailNow()
}
}
func TestSampleTests(t *testing.T) {
gtest.RunSubTests(t, &SampleTests{})
}
You can create as any test group you want within a package with each of them using a different set of setup/teardown routines.
Solution 9 - Unit Testing
You can use the testing package for test setup - which will set the stage for all tests and teardown - which will cleanup the stage after tests have run.
The below calculates the area of a rectangle:
package main
import (
"errors"
"fmt"
)
func area(height float64, width float64) (float64, error) {
if height == width {
fmt.Printf("Rectangle with given dimension is a square. Area is: %f\n", height * width)
return height * width, nil
} else if height <= 0 || width <= 0 {
return 0, errors.New("Both dimensions need to be positive")
} else {
fmt.Printf("Area is: %f\n", height * width)
return height * width, nil
}
}
func main() {
var length float64 = 4.0
var breadth float64 = 5.0
area(length, breadth)
}
This is the implementation for test setup and teardown using TestMain
as Salvador Dali's explains. (Note that since v1.15 the TestMain
function is no longer required to call os.Exit
[ref])
package main
import (
"log"
"testing"
)
var length float64
var breadth float64
func TestMain(m *testing.M) {
setup()
m.Run()
teardown()
}
func setup() {
length = 2.0
breadth = 3.0
log.Println("\n-----Setup complete-----")
}
func teardown() {
length = 0
breadth = 0
log.Println("\n----Teardown complete----")
}
func TestAreaOfRectangle(t *testing.T) {
val, err := area(length, breadth)
want := 6.0
if val != want && err != nil {
t.Errorf("Got %f and %v; Want %f and %v", val, err, want, nil)
}
}
And this is the implementation for test setup and teardown using sub-tests:
package main
import "testing"
func TestInvalidRectangle(t *testing.T) {
// setup
var length float64 = -2.0
var breadth float64 = 3.0
t.Log("\n-----Setup complete for invalid rectangle-----")
// sub-tests
t.Run("invalid dimensions return value", func(t *testing.T) {
val, _ := area(length, breadth)
area := 0.0
if val != area {
t.Errorf("Got %f; Want %f", val, area)
}
})
t.Run("invalid dimensions message", func(t *testing.T) {
_, err := area(length, breadth)
want := "Both dimensions need to be positive"
if err.Error() != want {
t.Errorf("Got error: %v; Want error: %v", err.Error(), want)
}
})
// teardown
t.Cleanup(func(){
length = 0
breadth = 0
t.Log("\n----Teardown complete for invalid rectangle----")
})
}
func TestRectangleIsSquare(t *testing.T) {
var length float64 = 3.0
var breadth float64 = 3.0
t.Log("\n-----Rectangle is square setup complete-----")
t.Run("valid dimensions value and message", func(t *testing.T) {
val, msg := area(length, breadth)
area := 9.0
if val != area && msg != nil {
t.Errorf("Got %f and %v; Want %f and %v", val, msg, area, nil)
}
})
t.Cleanup(func(){
length = 0
breadth = 0
t.Log("\n----Rectangle is square teardown Complete----")
})
}