C#
GO

C# to Go

10 lessons

Progress0%
1Introduction2Type System3Classes to Structs4Interfaces5Error Handling6Async to Goroutines7Generics8LINQ to Slices9Testing10Packages and Modules
All Mirror Courses
C#
GO
Testing
MirrorLesson 9 of 10
Lesson 9

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.

Mirror Card
C#
From C#:

In C#, you're familiar with go's built-in testing vs xunit/nunit — table-driven, benchmarks.

GO
In Go:

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:

GO
Go 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#:

C#
C# (What you know)
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"
Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

No framework needed — go test is built into the toolchain (no NuGet package required)

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

Table-driven tests (struct + t.Run) replace [Theory][InlineData]

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

No Assert class — use manual if + t.Error/t.Fatal/t.Errorf

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

Go errors are returned values — test with != nil, not Assert.Throws

Mirror Card
C#
From C#:

You may be used to different syntax or behavior.

GO
In Go:

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.

C#
C#
[Fact]
public void TestAdd() {
    Assert.Equal(3, _calc.Add(1, 2));
}
GO
Go
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.

C#
C#
[Theory]
[InlineData(1,2,3)]
[InlineData(4,5,9)]
void TestAdd(int a, int b, int want) { Assert.Equal(want, Add(a,b)); }
GO
Go
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.

C#
C#
Assert.Throws<DivideByZeroException>(() => calc.Divide(1, 0));
GO
Go
_, 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.

C#
C#
// xUnit: needs BenchmarkDotNet NuGet package
[Benchmark]
public int Add() => _calc.Add(1, 2);
GO
Go
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
}
// go test -bench=BenchmarkAdd -benchmem

Common 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
Common Pitfall
Don't assume Go works exactly like C#. While the concepts may be similar, the syntax and behavior can differ significantly.

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
Rule of Thumb
The best way to learn is by doing. Try rewriting some of your C# code in Go to practice these concepts.
PreviousNext