Appearance
测试概述
本指南介绍如何在 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/await | goroutine + channel |
| Mock | jest.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 testing4. 基准测试中忘记循环
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);
}
}最佳实践
使用表驱动测试
- 易于维护和扩展
- 测试用例清晰
使用子测试
- 组织相关测试
- 独立运行每个场景
使用 Helper 函数
- 减少重复代码
- 提高可读性
测试错误情况
- 不仅测试正常路径
- 测试边界条件和错误
使用 Cleanup
- 确保资源被清理
- 避免测试间相互影响
编写可读的错误消息
- 包含期望值和实际值
- 提供足够的上下文