Testing
Go's testing package vs Jest/Vitest — table-driven vs describe/it blocks
Introduction
In this lesson, you'll learn about testing in Go. Coming from TypeScript, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In TypeScript, you're familiar with go's testing package vs jest/vitest — table-driven vs describe/it blocks.
Go has its own approach to go's testing package vs jest/vitest — table-driven vs describe/it blocks, which we'll explore step by step.
The Go Way
Let's see how Go handles this concept. Here's a typical example:
// math_test.go
package math
import (
"testing"
"errors"
)
// Basic test (= it("adds two numbers"))
func TestAdd(t *testing.T) {
got := Add(1, 2)
if got != 3 {
t.Errorf("Add(1,2) = %d; want 3", got)
}
}
// Table-driven (= it.each / test.each)
func TestDivide(t *testing.T) {
tests := []struct {
a, b float64
want float64
wantErr bool
}{
{6, 3, 2, false},
{10, 5, 2, false},
{0, 4, 0, false},
{1, 0, 0, true}, // divide by zero
}
for _, tc := range tests {
t.Run(fmt.Sprintf("%v/%v", tc.a, tc.b), func(t *testing.T) {
got, err := Divide(tc.a, tc.b)
if tc.wantErr {
if err == nil { t.Error("expected error") }
return
}
if err != nil { t.Fatalf("unexpected: %v", err) }
if got != tc.want { t.Errorf("got %v; want %v", got, tc.want) }
})
}
}
// Setup/teardown via TestMain (= beforeAll/afterAll)
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
// Sub-tests with t.Run (= describe blocks)
func TestCalculator(t *testing.T) {
t.Run("accumulates", func(t *testing.T) {
c := NewCalculator()
c.Add(5); c.Add(3)
if c.Result() != 8 {
t.Errorf("got %d; want 8", c.Result())
}
})
}
// Run: go test ./...
// Verbose: go test -v ./...
// Specific: go test -run TestAdd ./...
// Coverage: go test -cover ./...Comparing to TypeScript
Here's how you might have written similar code in TypeScript:
// math.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { add, divide, Calculator } from "./math";
describe("math", () => {
it("adds two numbers", () => {
expect(add(1, 2)).toBe(3);
});
it.each([
[6, 3, 2],
[10, 5, 2],
[0, 4, 0],
])("divide(%i, %i) = %i", (a, b, expected) => {
expect(divide(a, b)).toBe(expected);
});
it("throws on divide by zero", () => {
expect(() => divide(1, 0)).toThrow("division by zero");
});
describe("Calculator", () => {
let calc: Calculator;
beforeEach(() => {
calc = new Calculator();
});
it("accumulates results", () => {
calc.add(5);
calc.add(3);
expect(calc.result).toBe(8);
});
});
});
// Run: npx vitest
// Watch: npx vitest --watch
// Coverage: npx vitest --coverageYou may be used to different syntax or behavior.
Go uses _test.go files; no framework needed (go test is built-in toolchain)
You may be used to different syntax or behavior.
Table-driven tests (slice + t.Run) replaces it.each/test.each
You may be used to different syntax or behavior.
t.Error continues; t.Fatal stops — Jest/Vitest always stops on expect() failure
You may be used to different syntax or behavior.
TestMain for global setup/teardown; t.Cleanup(fn) for per-test cleanup
You may be used to different syntax or behavior.
No built-in mock library — use interfaces for dependency injection instead
Step-by-Step Breakdown
1. Test Structure
Go test functions are TestXxx(t *testing.T). No describe/it nesting — use t.Run for sub-tests. Files must end in _test.go.
describe("add", () => {
it("returns sum", () => {
expect(add(1,2)).toBe(3);
});
});func TestAdd(t *testing.T) {
// no expect() — manual if checks
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 tests. Define cases in a struct slice, iterate with t.Run().
it.each([[1,2,3],[4,5,9]])("add %i+%i=%i",
(a,b,c) => expect(add(a,b)).toBe(c));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. Error vs Fatal
t.Error marks failure and continues. t.Fatal marks failure and stops the current test function. Unlike Jest which always stops on expect() failure.
expect(result).toBeDefined(); // stops on fail
expect(result.name).toBe("Alice");if result == nil { t.Fatal("nil result") } // stop
if result.Name != "Alice" {
t.Errorf("name=%q; want Alice", result.Name)
}4. Mocking with Interfaces
Go has no built-in mock library. Instead, define interfaces for dependencies and swap them with test implementations.
const mockDB = { query: vi.fn().mockResolvedValue([]) };
const svc = new UserService(mockDB);type DB interface { Query(string) ([]Row, error) }
type MockDB struct { Rows []Row }
func (m MockDB) Query(_ string) ([]Row, error) { return m.Rows, nil }
// In test:
svc := NewUserService(MockDB{Rows: testRows})Common Mistakes
When coming from TypeScript, developers often make these mistakes:
- Go uses _test.go files; no framework needed (go test is built-in toolchain)
- Table-driven tests (slice + t.Run) replaces it.each/test.each
- t.Error continues; t.Fatal stops — Jest/Vitest always stops on expect() failure
Key Takeaways
- _test.go files; TestXxx(t *testing.T); no framework — go test is in the toolchain
- Table-driven: struct slice + t.Run = it.each; t.Run also creates describe-like sub-tests
- t.Error continues; t.Fatal stops — choose based on whether remaining assertions make sense
- No mocks needed: define interfaces for deps and implement them in tests