TS
GO

TypeScript to Go

10 lessons

Progress0%
1Introduction: Transpiled to Compiled Binary2Type Systems: Structural Interfaces3Functions4Objects to Structs5Generics6Error Handling7Async to Goroutines8Ecosystem9Testing10Go Standard Library
All Mirror Courses
TS
GO
Testing
MirrorLesson 9 of 10
Lesson 9

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.

Mirror Card
TS
From TypeScript:

In TypeScript, you're familiar with go's testing package vs jest/vitest — table-driven vs describe/it blocks.

GO
In Go:

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:

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

TS
TypeScript (What you know)
// 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 --coverage
Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

GO
In Go:

Go uses _test.go files; no framework needed (go test is built-in toolchain)

Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

GO
In Go:

Table-driven tests (slice + t.Run) replaces it.each/test.each

Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

GO
In Go:

t.Error continues; t.Fatal stops — Jest/Vitest always stops on expect() failure

Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

GO
In Go:

TestMain for global setup/teardown; t.Cleanup(fn) for per-test cleanup

Mirror Card
TS
From TypeScript:

You may be used to different syntax or behavior.

GO
In Go:

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.

TS
TypeScript
describe("add", () => {
  it("returns sum", () => {
    expect(add(1,2)).toBe(3);
  });
});
GO
Go
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().

TS
TypeScript
it.each([[1,2,3],[4,5,9]])("add %i+%i=%i",
  (a,b,c) => expect(add(a,b)).toBe(c));
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. 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.

TS
TypeScript
expect(result).toBeDefined(); // stops on fail
expect(result.name).toBe("Alice");
GO
Go
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.

TS
TypeScript
const mockDB = { query: vi.fn().mockResolvedValue([]) };
const svc = new UserService(mockDB);
GO
Go
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})
Rule of Thumb
Accept interfaces in constructors/functions, not concrete types. This makes testing easy without a mock library.

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

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