-
Notifications
You must be signed in to change notification settings - Fork 2
Testing Basics
This section introduces testing in Go, which, unsurprisingly, is very much like writing any other kind of Go code.
In this section you’ll learn how to…
- use the
testing
package - use table-driven tests for multiple scenarios under the same test
- do setup/teardown for each test
Go's standard library comes equipped with all you need to test your programs, mainly through the testing package. Let's start with the simplest example of this section.
package main
import (
"testing"
"unicode"
)
func TestRuneIsDigit(t *testing.T) {
c := '4'
if unicode.IsDigit(c) != true {
t.Error("expected rune to be a digit")
}
}
=== RUN TestRuneIsDigit
--- PASS: TestRuneIsDigit (0.00s)
PASS
The output you see above is from running go test
inside of the folder where the test is located. The following is an example directory listing containing go files and matching test files:
.
├── somefile.go
├── somefile_test.go
├── main.go
In the listing above, somefile.go
contains some form of logic while by convention, somefile_test.go
contains the code that tests the functionality in somefile.go. Regardless of how you name the file, as long as it ends with _test.go
, the Go toolchain (go test
) will treat it as a test file and include it in test runs.
You may be used to setups and teardowns before and after each test from other languages and frameworks. Go has support for this concept through its TestMain function from the testing package.
Here's how it looks:
func TestMain(m *testing.M) {
// setup
code := m.Run()
// teardown os.Exit(code)
}
The idea is to perform any setup you need before calling on m.Run()
and performing any teardowns after it. In the snippet above, we capture and pass an exit code to os.Exit
.
We terminate the test run (and programs in general) with a zero (0) to indicate success or a non-zero for failure. Go here if you'd like to know more about exit code/status.
- Run a group of tests within a single test function.
- Useful for running related tests together, especially for multiple input scenarios to same function.
- Allow for parallelized tests within a single test function.
- Low overhead to add new scenarios.
- Easy to reproducy reported issues.
- Tip: Use a
map[string]...
instead of[]struct{...}
for the test cases to use map keys as test names in output.
package main
import (
"testing"
)
func Sum(nums []int) int {
total := 0
for _, num := range nums {
total += num
}
return total
}
func TestSumParallel(t *testing.T) {
tests := map[string]struct {
nums []int
expected int
}{
"positive numbers": {nums: []int{1, 2, 3}, expected: 6},
"negative numbers": {nums: []int{-1, -2, -3}, expected: -6},
"mix of positive and negative": {nums: []int{-1, 2, -3, 4}, expected: 2},
"zero values": {nums: []int{0, 0, 0}, expected: 0},
"empty slice": {nums: []int{}, expected: 0},
}
for name, tt := range tests {
tt := tt
t.Run(name, func(t *testing.T) {
t.Parallel()
result := Sum(tt.nums)
if result != tt.expected {
t.Errorf("Sum(%v) = %d; expected %d", tt.nums, result, tt.expected)
}
})
}
}
=== RUN TestSumParallel
=== RUN TestSumParallel/positive_numbers
=== PAUSE TestSumParallel/positive_numbers
=== RUN TestSumParallel/negative_numbers
=== PAUSE TestSumParallel/negative_numbers
=== RUN TestSumParallel/mix_of_positive_and_negative
=== PAUSE TestSumParallel/mix_of_positive_and_negative
=== RUN TestSumParallel/zero_values
=== PAUSE TestSumParallel/zero_values
=== RUN TestSumParallel/empty_slice
=== PAUSE TestSumParallel/empty_slice
=== CONT TestSumParallel/positive_numbers
=== CONT TestSumParallel/zero_values
=== CONT TestSumParallel/empty_slice
=== CONT TestSumParallel/mix_of_positive_and_negative
=== CONT TestSumParallel/negative_numbers
--- PASS: TestSumParallel (0.00s)
--- PASS: TestSumParallel/positive_numbers (0.00s)
--- PASS: TestSumParallel/zero_values (0.00s)
--- PASS: TestSumParallel/empty_slice (0.00s)
--- PASS: TestSumParallel/mix_of_positive_and_negative (0.00s)
--- PASS: TestSumParallel/negative_numbers (0.00s)
PASS
When you run tests with subtests marked to run in parallel, the PAUSE and CONT steps are part of the process that ensures the subtests are executed correctly and concurrently.
PAUSE
Indicates that the subtest has been initialized and is ready to run, but it is temporarily paused. This happens because the subtest is marked to run in parallel using t.Parallel(). Here's what happens during the PAUSE step:
- Initialization: The test framework initializes the subtest and sets up its context.
- Pause: The test framework initialize all parallel subtests and pauses them.
- Scheduling: The test framework decides when to run subtests, parallelizing their execution, maintaining proper synchronization and resource management.
CONT
Indicates that the previously paused subtest is now being continued and executed. This is the point where the subtest actually runs its test logic. Here’s what happens during the CONT step:
- Continuation: The test framework resumes the execution of the subtest. This happens after all parallel subtests have been initialized and paused.
- Execution: The subtest runs its test logic, checking conditions, and making assertions.
- Completion: Once the subtest finishes its execution, it reports the result (pass or fail).
-
go test
setspwd
as package directory so you can open files easily from within your tests (i.e.os.Open("fixtures/test-data.json")
) - Settle on and use a relative path (e.g.
fixtures
) directory as a place to store test data - Useful for loading config, model data, binary data, etc
- Use to compare complex test output against an expected file without hardcoding
- Scalable way to test complex structures (write a String() method and compare the resulting strings)
var update = flag.Bool("update", false, "update golden files")
func TestSum(t *testing.T) {
// ...table
for _, tc := range cases {
actual := doSomething
golden := filepath.Join("fixtures", tc.Name+"golden")
if *update{
os.WriteFile(golden, actual, 0644)
}
expected, _ ioutil.ReadFile(golden)
if !bytes.Equal(actual, expected) {
// fail test
}
}
}
You can introduce flags to the go test
command!
$ go test -update