Skip to content

测试概述

本指南介绍如何在 Targo 中编写和运行测试。

目标读者

  • 想要为 Targo 代码编写测试的开发者
  • 熟悉 TypeScript 测试框架的开发者
  • 需要了解 Go 测试模型的开发者

核心概念

Targo 使用 Go 的测试框架,允许你使用 TypeScript 语法编写测试:

  • 测试文件 - 以 _test.ts.test.ts 结尾
  • 测试函数 - 以 Test 开头的导出函数
  • testing 包 - 提供测试工具和断言
  • go test - 编译后使用 Go 的测试运行器

与 TypeScript 的区别

TypeScript 通常使用 Jest、Mocha 等测试框架。Targo 使用 Go 的内置测试框架,更简单但功能强大。

测试文件命名

测试文件必须遵循以下命名约定之一:

  • *_test.ts (推荐,符合 Go 约定)
  • *.test.ts (TypeScript 约定)

两种模式都会编译为 *_test.go

示例

math_test.ts  → math_test.go  ✓ 推荐
math.test.ts  → math_test.go  ✓ 也可以

测试函数签名

单元测试

单元测试必须遵循以下签名:

typescript
import { testing } from "testing";

export function TestFunctionName(t: testing.T): void {
    // 测试代码
}

要求

  • 函数名必须以 Test 开头,后跟大写字母
  • 必须导出(export
  • 必须有一个 testing.T 类型的参数
  • 必须返回 void

示例

typescript
import { testing } from "testing";

export function TestAdd(t: testing.T): void {
    let result = add(2, 3);
    if (result != 5) {
        t.Errorf("add(2, 3) = %d; want 5", result);
    }
}

编译为 Go:

go
func TestAdd(t *testing.T) {
    result := add(2, 3)
    if result != 5 {
        t.Errorf("add(2, 3) = %d; want 5", result)
    }
}

基准测试

基准测试用于测量性能:

typescript
export function BenchmarkFunctionName(b: testing.B): void {
    for (let i = 0; i < b.N; i++) {
        // 要测试的代码
    }
}

要求

  • 函数名必须以 Benchmark 开头
  • 必须导出
  • 必须有一个 testing.B 类型的参数
  • 必须循环 b.N

示例

typescript
export function BenchmarkAdd(b: testing.B): void {
    for (let i = 0; i < b.N; i++) {
        add(1, 2);
    }
}

示例测试

示例测试用于文档和演示:

typescript
export function ExampleFunctionName(): void {
    console.log("output");
    // Output: output
}

要求

  • 函数名必须以 Example 开头
  • 必须导出
  • 无参数
  • 必须返回 void

测试方法

错误报告

testing.T 提供以下方法报告错误:

typescript
// 报告错误并继续
t.Error("something went wrong");
t.Errorf("expected %d, got %d", expected, actual);

// 报告错误并停止测试
t.Fatal("critical error");
t.Fatalf("expected %d, got %d", expected, actual);

日志记录

typescript
// 记录日志(只在 -v 模式下显示)
t.Log("debug info");
t.Logf("value: %d", value);

测试控制

typescript
// 跳过测试
t.Skip("skipping this test");
t.SkipNow();

// 检查测试是否失败
if (t.Failed()) {
    // 清理...
}

子测试

使用 t.Run() 创建子测试:

typescript
export function TestUser(t: testing.T): void {
    t.Run("creation", (t: testing.T) => {
        let user = new User("Alice");
        if (user.Name != "Alice") {
            t.Errorf("Expected name Alice, got %s", user.Name);
        }
    });
    
    t.Run("validation", (t: testing.T) => {
        let user = new User("");
        if (user.IsValid()) {
            t.Error("Empty name should be invalid");
        }
    });
}

表驱动测试

表驱动测试是 Go 的最佳实践,用于测试多个场景:

typescript
import { testing } from "testing";

interface TestCase {
    name: string;
    a: int;
    b: int;
    want: int;
}

export function TestAddTable(t: testing.T): void {
    let tests: TestCase[] = [
        { name: "positive numbers", a: 2, b: 3, want: 5 },
        { name: "zero", a: 0, b: 0, want: 0 },
        { name: "negative", a: -1, b: 1, want: 0 },
        { name: "large numbers", a: 1000, b: 2000, want: 3000 }
    ];
    
    for (const tt of tests) {
        t.Run(tt.name, (t: testing.T) => {
            let got = add(tt.a, tt.b);
            if (got != tt.want) {
                t.Errorf("add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want);
            }
        });
    }
}

优点

  • 易于添加新测试用例
  • 每个用例独立运行
  • 输出中显示清晰的测试名称
  • 失败时显示具体哪个用例失败

运行测试

基本用法

bash
# 运行所有测试
targo test

# 运行特定测试
targo test --run TestAdd

# 运行匹配模式的测试
targo test --run "TestUser.*"

# 详细输出
targo test -v

基准测试

bash
# 运行所有基准测试
targo test --bench .

# 运行特定基准测试
targo test --bench BenchmarkAdd

# 显示内存分配统计
targo test --bench . --benchmem

覆盖率

bash
# 生成覆盖率报告
targo test --cover

# 生成覆盖率文件
targo test --coverprofile=coverage.out

# 在浏览器中查看覆盖率
go tool cover -html=coverage.out

测试辅助函数

Helper 函数

使用 t.Helper() 标记辅助函数:

typescript
function assertEqual(t: testing.T, got: int, want: int): void {
    t.Helper();  // 标记为辅助函数
    if (got != want) {
        t.Errorf("got %d; want %d", got, want);
    }
}

export function TestCalculation(t: testing.T): void {
    let result = calculate(10);
    assertEqual(t, result, 20);  // 错误会指向这一行
}

清理函数

使用 t.Cleanup() 注册清理函数:

typescript
export function TestWithCleanup(t: testing.T): void {
    let file = createTempFile();
    t.Cleanup(() => {
        file.Close();
        os.Remove(file.Name());
    });
    
    // 使用 file...
    // 测试结束后自动清理
}

临时目录

使用 t.TempDir() 创建临时目录:

typescript
export function TestFileOperations(t: testing.T): void {
    let dir = t.TempDir();  // 自动清理
    
    let path = filepath.Join(dir, "test.txt");
    os.WriteFile(path, Slice.from("content"), 0644);
    
    // 测试文件操作...
}

实际示例

测试 HTTP 处理器

typescript
import { testing } from "testing";
import { http } from "net/http";
import { httptest } from "net/http/httptest";

function helloHandler(w: http.ResponseWriter, r: http.Request): void {
    w.Write(Slice.from("Hello, World!"));
}

export function TestHelloHandler(t: testing.T): void {
    // 创建测试请求
    let req = httptest.NewRequest("GET", "/", null);
    let w = httptest.NewRecorder();
    
    // 调用处理器
    helloHandler(w, req);
    
    // 检查响应
    let resp = w.Result();
    if (resp.StatusCode != 200) {
        t.Errorf("Expected status 200, got %d", resp.StatusCode);
    }
    
    let [body, _] = io.ReadAll(resp.Body);
    let bodyStr = string(body);
    if (bodyStr != "Hello, World!") {
        t.Errorf("Expected 'Hello, World!', got '%s'", bodyStr);
    }
}

测试错误处理

typescript
import { testing } from "testing";
import { errors } from "errors";

function divide(a: int, b: int): [int, error | null] {
    if (b == 0) {
        return [0, errors.New("division by zero")];
    }
    return [a / b, null];
}

export function TestDivide(t: testing.T): void {
    t.Run("normal division", (t: testing.T) => {
        let [result, err] = divide(10, 2);
        if (err != null) {
            t.Fatalf("Unexpected error: %v", err);
        }
        if (result != 5) {
            t.Errorf("Expected 5, got %d", result);
        }
    });
    
    t.Run("division by zero", (t: testing.T) => {
        let [_, err] = divide(10, 0);
        if (err == null) {
            t.Fatal("Expected error, got nil");
        }
    });
}

测试并发代码

typescript
import { testing } from "testing";
import { sync } from "sync";

class Counter {
    mu: sync.Mutex = new sync.Mutex();
    value: int = 0;
    
    Increment(): void {
        this.mu.Lock();
        defer(() => this.mu.Unlock());
        this.value++;
    }
    
    Value(): int {
        this.mu.Lock();
        defer(() => this.mu.Unlock());
        return this.value;
    }
}

export function TestCounterConcurrent(t: testing.T): void {
    let counter = new Counter();
    let wg = new sync.WaitGroup();
    
    // 启动 100 个 goroutine
    for (let i = 0; i < 100; i++) {
        wg.Add(1);
        go(() => {
            defer(() => wg.Done());
            counter.Increment();
        });
    }
    
    wg.Wait();
    
    let value = counter.Value();
    if (value != 100) {
        t.Errorf("Expected 100, got %d", value);
    }
}

TypeScript 对比

测试框架

特性TypeScript (Jest)Targo
测试文件*.test.ts*_test.ts*.test.ts
测试函数test() / it()export function Test*()
断言expect().toBe()t.Errorf() / t.Fatal()
子测试describe()t.Run()
异步测试async/awaitgoroutine + channel
Mockjest.mock()接口 + 依赖注入

代码对比

TypeScript (Jest)

typescript
describe('Calculator', () => {
    test('adds two numbers', () => {
        expect(add(2, 3)).toBe(5);
    });
    
    test('handles zero', () => {
        expect(add(0, 0)).toBe(0);
    });
});

Targo

typescript
export function TestCalculator(t: testing.T): void {
    t.Run("adds two numbers", (t: testing.T) => {
        let result = add(2, 3);
        if (result != 5) {
            t.Errorf("Expected 5, got %d", result);
        }
    });
    
    t.Run("handles zero", (t: testing.T) => {
        let result = add(0, 0);
        if (result != 0) {
            t.Errorf("Expected 0, got %d", result);
        }
    });
}

常见陷阱

1. 忘记导出测试函数

typescript
// ❌ 错误:未导出
function TestAdd(t: testing.T): void {
    // 不会被运行
}

// ✅ 正确:导出
export function TestAdd(t: testing.T): void {
    // 会被运行
}

2. 错误的函数签名

typescript
// ❌ 错误:缺少参数
export function TestAdd(): void {
    // 编译错误
}

// ❌ 错误:错误的参数类型
export function TestAdd(t: any): void {
    // 编译错误
}

// ✅ 正确
export function TestAdd(t: testing.T): void {
    // 正确
}

3. 忘记绑定 testing 包

bash
# 首次使用前需要绑定
targo bind testing

4. 基准测试中忘记循环

typescript
// ❌ 错误:没有循环 b.N 次
export function BenchmarkAdd(b: testing.B): void {
    add(1, 2);  // 只运行一次
}

// ✅ 正确
export function BenchmarkAdd(b: testing.B): void {
    for (let i = 0; i < b.N; i++) {
        add(1, 2);
    }
}

最佳实践

  1. 使用表驱动测试

    • 易于维护和扩展
    • 测试用例清晰
  2. 使用子测试

    • 组织相关测试
    • 独立运行每个场景
  3. 使用 Helper 函数

    • 减少重复代码
    • 提高可读性
  4. 测试错误情况

    • 不仅测试正常路径
    • 测试边界条件和错误
  5. 使用 Cleanup

    • 确保资源被清理
    • 避免测试间相互影响
  6. 编写可读的错误消息

    • 包含期望值和实际值
    • 提供足够的上下文

下一步

参考