05open 25 min

Testing in Go: table-driven tests and standard-library-first workflows

Translate Rust test habits into Go's testing package and table-driven style.

by the end of this lesson you can

  • Uses testing.T clearly
  • Chooses a table-driven loop for related cases
  • Uses explicit failure messages instead of macro-style assertions

Overview

Rust developers already know explicit tests with built-in tooling. Go testing feels similarly plain, but the dominant idioms are different: _test.go files, testing.T, and case tables instead of test modules and assertion macros.

In Rust, you often

write #[test] functions near the code and use assertion macros to express expected behavior.

In Go, the common pattern is

to use the standard testing package, plain failure messages, and table-driven tests when many related cases share one shape.

why this difference matters

This lesson should help Rust developers feel productive quickly without importing heavier testing patterns too early.

Rust

#[test]
fn square_returns_16() {
    assert_eq!(square(4), 16);
}

Go

func TestSquare(t *testing.T) {
    got := square(4)
    want := 16
    if got != want {
        t.Fatalf("got %d, want %d", got, want)
    }
}

Deeper comparison

Rust version

#[test]
fn slugify_cases() {
    let cases = [("Hello World", "hello-world")];
    for (input, expected) in cases {
        assert_eq!(slugify(input), expected);
    }
}

Go version

func TestSlugify(t *testing.T) {
    cases := []struct {
        in   string
        want string
    }{{"Hello World", "hello-world"}}

    for _, tc := range cases {
        if got := slugify(tc.in); got != tc.want {
            t.Fatalf("got %q, want %q", got, tc.want)
        }
    }
}

Reflect

Why do table-driven tests feel natural in Go even though they are less macro-driven than Rust tests?

what a strong answer notices

A strong answer mentions scannability, low ceremony, and easy grouping of related inputs and expected outputs.

Rewrite

Rewrite this Rust test into Go using the standard testing package.

Rewrite this Rust

#[test]
fn double_cases() {
    for (input, expected) in [(2, 4), (3, 6)] {
        assert_eq!(double(input), expected);
    }
}

what good looks like

  • Uses testing.T clearly
  • Chooses a table-driven loop for related cases
  • Uses explicit failure messages instead of macro-style assertions

Practice

Design a table-driven Go test for a username normalizer with valid and invalid inputs.

success criteria

  • Uses a clear case table
  • Keeps the loop easy to scan
  • Uses failure messages that make mismatches obvious

Common mistakes

  • Expecting Rust-style assertion ergonomics before learning Go's standard shape.
  • Writing many near-duplicate tests instead of a simple case table.
  • Adding third-party test helpers before the standard package is comfortable.

takeaways

  • This lesson should help Rust developers feel productive quickly without importing heavier testing patterns too early.
  • A strong answer mentions scannability, low ceremony, and easy grouping of related inputs and expected outputs.
  • Uses a clear case table