Testing
Go's built-in testing vs JUnit 5 — table-driven tests, benchmarks
Introduction
In this lesson, you'll learn about testing in Go. Coming from Java, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Java, you're familiar with go's built-in testing vs junit 5 — table-driven tests, benchmarks.
Go has its own approach to go's built-in testing vs junit 5 — table-driven tests, benchmarks, which we'll explore step by step.
The Go Way
Let's see how Go handles this concept. Here's a typical example:
// calculator_test.go
package calculator
import (
"testing"
"errors"
)
// Basic test (= @Test)
func TestAdd(t *testing.T) {
got := Add(1, 2)
if got != 3 {
t.Errorf("Add(1,2) = %d; want 3", got)
}
}
// Table-driven test (= @ParameterizedTest @CsvSource)
func TestAddParameterized(t *testing.T) {
tests := []struct {
a, b, want int
}{
{1, 2, 3},
{4, 5, 9},
{-1, 1, 0},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%d+%d", tc.a, tc.b), func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.want {
t.Errorf("got %d; want %d", got, tc.want)
}
})
}
}
// Test error case
func TestDivideByZero(t *testing.T) {
_, err := Divide(1, 0)
if err == nil {
t.Error("expected error for divide by zero")
}
}
// TestMain = @BeforeAll/@AfterAll for the whole package
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
// Benchmark (no JUnit equivalent)
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// go test ./... = mvn test
// go test -run TestAdd ./.. = mvn test -Dtest=TestAdd
// go test -bench=. = no JUnit equivalentComparing to Java
Here's how you might have written similar code in Java:
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calc;
@BeforeEach
void setUp() {
calc = new Calculator();
}
@Test
void testAdd() {
assertEquals(3, calc.add(1, 2));
assertEquals(0, calc.add(-1, 1));
}
@ParameterizedTest
@CsvSource({"1,2,3", "4,5,9", "-1,1,0"})
void testAddParameterized(int a, int b, int expected) {
assertEquals(expected, calc.add(a, b));
}
@Test
void testDivideByZero() {
assertThrows(ArithmeticException.class,
() -> calc.divide(1, 0));
}
@AfterEach
void tearDown() { /* cleanup */ }
}
// Run: mvn test
// Run specific: mvn test -Dtest=CalculatorTest#testAddYou may be used to different syntax or behavior.
No framework needed — 'go test' is built into the toolchain
You may be used to different syntax or behavior.
Table-driven tests (struct slice + t.Run) replace @ParameterizedTest @CsvSource
You may be used to different syntax or behavior.
TestMain replaces @BeforeAll/@AfterAll for package-level setup/teardown
You may be used to different syntax or behavior.
No assertions library — use manual if checks with t.Error/t.Errorf/t.Fatal
You may be used to different syntax or behavior.
Benchmarks (BenchmarkXxx) are built-in — JUnit needs external benchmark library
Step-by-Step Breakdown
1. Test Function Structure
Go test functions are TestXxx(t *testing.T). No class, no annotations. Files end in _test.go — excluded from production build.
@Test
void testAdd() {
assertEquals(3, calc.add(1, 2));
}func TestAdd(t *testing.T) {
if got := Add(1, 2); got != 3 {
t.Errorf("Add(1,2) = %d; want 3", got)
}
}2. Table-Driven Tests
Table-driven tests are the Go idiom for parametrized testing. Each test case is a struct — t.Run creates a sub-test for each.
@ParameterizedTest
@CsvSource({"1,2,3", "4,5,9"})
void test(int a, int b, int want) { assertEquals(want, add(a,b)); }tests := []struct{ a,b,want int }{{1,2,3},{4,5,9}}
for _, tc := range tests {
t.Run(fmt.Sprintf("%d+%d",tc.a,tc.b), func(t *testing.T){
if got:=Add(tc.a,tc.b); got!=tc.want { t.Fail() }
})
}3. Error vs Fatal
t.Error marks failed and continues; t.Fatal marks failed and stops. Unlike JUnit where assertEquals always stops on failure.
assertEquals(expected, actual); // JUnit stops here on failif result == nil { t.Fatal("nil result") } // stops
if result.Name != "Bob" {
t.Errorf("name=%q; want Bob", result.Name) // continues
}4. Benchmarks
Go benchmarks measure performance with b.N iterations — the framework adjusts N for stable results.
// JUnit: @BenchmarkMode(Mode.AverageTime) with JMH libraryfunc BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// go test -bench=. -benchmem
// BenchmarkAdd-8 1000000000 0.25 ns/op 0 B/opCommon Mistakes
When coming from Java, developers often make these mistakes:
- No framework needed — 'go test' is built into the toolchain
- Table-driven tests (struct slice + t.Run) replace @ParameterizedTest @CsvSource
- TestMain replaces @BeforeAll/@AfterAll for package-level setup/teardown
Key Takeaways
- _test.go files; TestXxx(t *testing.T) — no framework or annotations
- Table-driven: struct slice + t.Run = @ParameterizedTest equivalent
- t.Error continues on fail; t.Fatal stops — choose based on whether remaining checks are valid
- Benchmarks built-in: BenchmarkXxx(b *testing.B); go test -bench=. -benchmem