AICollection Help

Built-in Testing

Go’s built-in testing framework, located in the testing package, provides tools to write unit tests, benchmarks, and example-based documentation. Testing is a core part of Go's ecosystem, and its simplicity encourages developers to adopt testing in their projects.

Key Features of Go's Testing Framework

  1. Test Files:

    • Test files are named with the _test.go suffix (e.g., math_test.go).

    • These files are excluded from normal builds.

  2. Test Functions:

    • A test function starts with Test and takes a single argument of type *testing.T.

    • Test functions are automatically run by the go test command.

  3. Assertion:

    • Go doesn’t have built-in assertions; you use conditions with t.Error or t.Fatal to fail tests.

  4. Benchmarks:

    • Functions start with Benchmark and take *testing.B as a parameter. These are used for performance testing.

  5. Examples:

    • Functions start with Example. Output can be verified for correctness in documentation.

Writing Unit Tests

Basic Unit Test

package math import "testing" func Add(a, b int) int { return a + b } func TestAdd(t *testing.T) { result := Add(2, 3) expected := 5 if result != expected { t.Errorf("Add(2, 3) = %d; want %d", result, expected) } }

Explanation:

  • t.Errorf logs the error without stopping the test.

  • If the test passes, no output is shown by default.

Using t.Fatal

func TestAddWithFatal(t *testing.T) { result := Add(2, 3) if result != 5 { t.Fatal("Test failed: Add(2, 3) != 5") } // This code will not execute if Fatal is called t.Log("Test passed") }

Key Difference:

  • t.Fatal immediately stops the test, while t.Error allows the test to continue.

Table-Driven Tests

Table-driven tests allow testing multiple scenarios with less code repetition.

func TestAddTableDriven(t *testing.T) { tests := []struct { a, b, expected int }{ {1, 2, 3}, {2, 3, 5}, {-1, -1, -2}, {0, 0, 0}, } for _, tt := range tests { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } } }

Explanation:

  • Table-driven tests iterate through a set of input-output pairs, ensuring code coverage for multiple scenarios.

Benchmark Testing

Benchmarks help measure the performance of functions. Benchmark functions start with Benchmark and take *testing.B.

Basic Benchmark

func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(2, 3) } }

Explanation:

  • b.N determines the number of iterations. The testing framework adjusts this to get reliable results.

Benchmark with Allocations

To analyze memory allocation, use b.ReportAllocs.

func BenchmarkAddWithAlloc(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { _ = Add(2, 3) } }

Output:

BenchmarkAddWithAlloc-8 1000000000 0.25 ns/op 0 B/op 0 allocs/op

Explanation:

  • b.ReportAllocs reports memory allocations per operation.

Example Functions

Example functions are used for documentation and testing. If the output is included in a comment, it is verified by go test.

Example Function

func ExampleAdd() { fmt.Println(Add(2, 3)) // Output: 5 }

Usage:

  • Example functions are included in Go documentation generated by godoc.

Test Suites with setup and teardown

Go doesn't have built-in support for setup and teardown, but you can implement them using helper functions.

Setup and Teardown

var globalData int func setup() { globalData = 42 } func teardown() { globalData = 0 } func TestWithSetup(t *testing.T) { setup() defer teardown() if globalData != 42 { t.Errorf("Expected globalData to be 42; got %d", globalData) } }

Explanation:

  • Use defer to ensure teardown runs after the test.

Skipping Tests

Use t.Skip to conditionally skip tests.

Example 1

func TestSkip(t *testing.T) { if true { // Replace with actual condition t.Skip("Skipping this test") } t.Error("This code will not run") }

Output:

--- SKIP: TestSkip (0.00s) main_test.go:5: Skipping this test

Subtests

Subtests group related tests and improve output readability.

Example 2

func TestAddSubtests(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive numbers", 1, 2, 3}, {"negative numbers", -1, -2, -3}, {"zeros", 0, 0, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }) } }

Output:

=== RUN TestAddSubtests === RUN TestAddSubtests/positive_numbers === RUN TestAddSubtests/negative_numbers === RUN TestAddSubtests/zeros --- PASS: TestAddSubtests (0.00s) --- PASS: TestAddSubtests/positive_numbers (0.00s) --- PASS: TestAddSubtests/negative_numbers (0.00s) --- PASS: TestAddSubtests/zeros (0.00s)

Testing with Mocking

Use interfaces and dependency injection to mock components in tests.

Mock Example

type MathService interface { Add(a, b int) int } type RealMathService struct{} func (s RealMathService) Add(a, b int) int { return a + b } type MockMathService struct{} func (s MockMathService) Add(a, b int) int { return 42 // Mocked result } func TestMockService(t *testing.T) { var svc MathService = MockMathService{} result := svc.Add(1, 2) if result != 42 { t.Errorf("Expected 42; got %d", result) } }

Running Tests

Run tests, benchmarks, and examples using the go test command:

go test -v ./... go test -run TestAdd ./... go test -bench . ./...

Best Practices for Testing in Go

  1. Keep Tests Small:

    • Test one behavior at a time to isolate failures.

  2. Use Table-Driven Tests:

    • Simplify repetitive testing logic.

  3. Mock Dependencies:

    • Use interfaces for testable and decoupled code.

  4. Write Benchmarks:

    • Measure and optimize performance-critical code.

  5. Test Edge Cases:

    • Include tests for unusual or boundary inputs.

  6. Automate Tests:

    • Use CI/CD pipelines to run tests automatically.

Conclusion

Go's testing framework provides everything needed to write unit tests, benchmarks, and examples, all within the standard library. Its simplicity encourages developers to test effectively, focusing on code correctness and performance. By adopting practices like table-driven tests, mocking, and subtests, you can write maintainable and robust test suites.

Last modified: 29 December 2024