Testing
Go's built-in testing vs xUnit/NUnit — table-driven, benchmarks
Introduction
In this lesson, you'll learn about testing in Go. Coming from C#, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C#, you're familiar with go's built-in testing vs xunit/nunit — table-driven, benchmarks.
Go has its own approach to go's built-in testing vs xunit/nunit — table-driven, 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 (
"fmt"
"testing"
)
// [Fact] equivalent
func TestAdd_TwoPositives_ReturnsSum(t *testing.T) {
calc := NewCalculator()
if got := calc.Add(1, 2); got != 3 {
t.Errorf("Add(1,2) = %d; want 3", got)
}
}
// [Theory][InlineData] equivalent — table-driven
func TestAdd_Parametrized(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) {
calc := NewCalculator()
if got := calc.Add(tc.a, tc.b); got != tc.want {
t.Errorf("got %d; want %d", got, tc.want)
}
})
}
}
// Error case — check error returned
func TestDivide_ByZero_ReturnsError(t *testing.T) {
_, err := NewCalculator().Divide(1, 0)
if err == nil {
t.Error("expected error for divide by zero")
}
}
// Benchmark (no xUnit equivalent)
func BenchmarkAdd(b *testing.B) {
calc := NewCalculator()
for i := 0; i < b.N; i++ {
calc.Add(1, 2)
}
}
// go test ./...
// go test -run TestAdd -v ./...
// go test -bench=. -benchmem ./...Comparing to C#
Here's how you might have written similar code in C#:
using Xunit;
using System.Collections.Generic;
public class CalculatorTests
{
private readonly Calculator _calc = new();
[Fact]
public void Add_TwoPositives_ReturnsSum()
{
Assert.Equal(3, _calc.Add(1, 2));
}
[Theory]
[InlineData(1, 2, 3)]
[InlineData(4, 5, 9)]
[InlineData(-1, 1, 0)]
public void Add_Parametrized(int a, int b, int expected)
{
Assert.Equal(expected, _calc.Add(a, b));
}
[Fact]
public void Divide_ByZero_ThrowsException()
{
Assert.Throws<DivideByZeroException>(
() => _calc.Divide(1, 0));
}
}
// Run: dotnet test
// Run specific: dotnet test --filter "FullyQualifiedName~Add"You may be used to different syntax or behavior.
No framework needed — go test is built into the toolchain (no NuGet package required)
You may be used to different syntax or behavior.
Table-driven tests (struct + t.Run) replace [Theory][InlineData]
You may be used to different syntax or behavior.
No Assert class — use manual if + t.Error/t.Fatal/t.Errorf
You may be used to different syntax or behavior.
Go errors are returned values — test with != nil, not Assert.Throws
You may be used to different syntax or behavior.
Benchmarks are built-in; xUnit needs external BenchmarkDotNet library
Step-by-Step Breakdown
1. Test Structure
Go test functions are TestXxx(t *testing.T). No class, no attributes. test files end in _test.go.
[Fact]
public void TestAdd() {
Assert.Equal(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 replace [Theory][InlineData]. They're more flexible — any struct fields as test parameters.
[Theory]
[InlineData(1,2,3)]
[InlineData(4,5,9)]
void TestAdd(int a, int b, int want) { Assert.Equal(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 Add(tc.a,tc.b) != tc.want { t.Fail() }
})
}3. Testing Errors
Go functions return errors instead of throwing. Test by checking err != nil, not Assert.Throws.
Assert.Throws<DivideByZeroException>(() => calc.Divide(1, 0));_, err := Divide(1, 0)
if err == nil {
t.Error("expected error for divide by zero")
}4. Benchmarks
Go benchmarks are BenchmarkXxx(b *testing.B). b.N is adjusted by the framework for stable results — no external library needed.
// xUnit: needs BenchmarkDotNet NuGet package
[Benchmark]
public int Add() => _calc.Add(1, 2);func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
// go test -bench=BenchmarkAdd -benchmemCommon Mistakes
When coming from C#, developers often make these mistakes:
- No framework needed — go test is built into the toolchain (no NuGet package required)
- Table-driven tests (struct + t.Run) replace [Theory][InlineData]
- No Assert class — use manual if + t.Error/t.Fatal/t.Errorf
Key Takeaways
- TestXxx(t *testing.T) in _test.go — no framework, no NuGet package
- Table-driven: struct slice + t.Run = [Theory][InlineData]
- Test errors with err != nil; no Assert.Throws (Go errors are return values)
- Benchmarks built-in: BenchmarkXxx(b *testing.B); go test -bench=. -benchmem