返回首页

Go 中的 fixture 用于自动测试:使用 Axiom 实践

本文介绍了如何在 Go 中使用 fixture 从自动测试中提取基础设施逻辑。它涵盖了 Axiom 库、其架构以及在集成场景中的实用示例。

Go fixture:如何编写干净的集成测试
Advertisement 728x90

Go 中的 Fixture:使用 Axiom 从自动化测试中提取基础设施

Go 中的集成测试和端到端(E2E)测试常常将业务逻辑与基础设施管理混在一起。标准 testing 包没有提供 fixture 机制,这迫使开发者在测试中直接重复编写初始化客户端、准备数据和清理资源的代码。这种做法降低了可读性,增加了维护难度,并阻碍了测试套件的扩展。解决方案是通过第三方库如 Axiom 来实现 fixture,它允许以声明式方式描述具有管理生命周期的依赖关系。

测试中的 Fixture 是什么

Fixture 不仅仅是一个辅助函数——它是一种具有明确生命周期的资源:按需创建、测试执行后自动清理、能够依赖其他 fixture,以及与断言逻辑隔离。与手动管理连接或数据不同,fixture 提供了以下特性:

  • 延迟初始化——资源仅在首次访问时创建。
  • 自动清理——即使测试失败,状态也会被保证清理。
  • 声明式依赖——一个 fixture 可以引用另一个,而无需手动传递参数。
  • 测试间隔离和重试安全——状态不会在尝试之间泄漏。

像 Python(pytest)或 Java(JUnit)这样的语言内置了这些机制。在 Go 中,必须显式实现或使用专用工具。

Google AdInline article slot

没有 Fixture 时的痛点:测试内部的基础设施

没有 fixture 的典型 Go 测试看起来像这样:

func TestGetUser(t *testing.T) {
    client, err := NewUserClient()
    if err != nil {
        t.Fatalf("failed to create client: %v", err)
    }
    defer client.Close()

    user, err := CreateUser(client)
    if err != nil {
        t.Fatalf("failed to create user: %v", err)
    }
    defer func() {
        _ = DeleteUser(client, user.ID)
    }()

    resp, err := client.GetUser(user.ID)
    if err != nil {
        t.Fatalf("failed to get user: %v", err)
    }

    if resp.ID != user.ID {
        t.Fatalf("unexpected user id")
    }
}

在这里,从创建客户端到删除用户的一切都写在测试主体中。随着依赖数量增加(数据库、外部 API、消息队列),这种方法会导致:

  • 代码重复度高。
  • 修改困难(例如,更新客户端认证)。
  • 清理逻辑容易出错。
  • 可读性差:核心断言淹没在基础设施细节中。

使用 Axiom 实现 Fixture

Axiom 库提供了一种机制,用于将 fixture 描述为返回值、清理函数和错误函数。生命周期通过 Runner 和 Case 来管理。

Google AdInline article slot

示例结构:

// Fixture client
func UserClientFixture(_ *axiom.Config) (any, func(), error) {
    client, err := NewUserClient()
    if err != nil {
        return nil, nil, err
    }
    return client, client.Close, nil
}

// Fixture polzovatelya, zavisyaschaya from client
func UserFixture(cfg *axiom.Config) (any, func(), error) {
    client := axiom.GetFixture*UserClient
    user, err := client.CreateUser()
    if err != nil {
        return nil, nil, err
    }
    cleanup := func() { _ = client.DeleteUser(user.ID) }
    return user, cleanup, nil
}

// Runner with globalnoy fiksturoy
var runner = axiom.NewRunner(
    axiom.WithRunnerFixture("client", UserClientFixture),
)

// Test
func TestGetUser(t *testing.T) {
    c := axiom.NewCase(
        axiom.WithCaseName("get user by id"),
        axiom.WithCaseFixture("user", UserFixture),
    )

    runner.RunCase(t, c, func(cfg *axiom.Config) {
        client := axiom.GetFixture*UserClient
        user := axiom.GetFixture*User

        resp, err := client.GetUser(user.ID)
        if err != nil {
            t.Fatalf("failed to get user: %v", err)
        }
        if resp.ID != user.ID {
            t.Fatalf("unexpected user id")
        }
    })
}

这种方法的 klyuch 优势:

  • 职责分离清晰:fixture 处理基础设施,测试处理行为。
  • 可重用性:单个客户端 fixture 可通过 Runner 在所有测试中使用。
  • 重试安全:测试重试时,所有 fixture 都会重新创建,避免先前状态影响。
  • 声明式:依赖关系明确声明,而测试主体中无需命令式代码。

何时实现 Fixture

Fixture 在以下场景中特别有用:

Google AdInline article slot
  • 具有多个外部依赖的集成测试(数据库、API、文件存储)。
  • 需要复杂环境搭建的端到端(E2E)测试。
  • 测试数量庞大的项目(>50–100 个),维护至关重要。
  • 追求测试基础设施统一性和可预测性的团队。

对于可以通过接口轻松模拟依赖的简单单元测试,不要使用 fixture。

关键要点

  • Go 没有内置 fixture——需要显式实现或使用像 Axiom 这样的库。
  • Fixture 的目标是从测试主体中抽离基础设施逻辑。
  • Axiom 提供延迟初始化、自动清理以及测试间隔离。
  • 该方法在大项目中特别有效,尤其是数百个集成测试。
  • Fixture 不会取代 mock——它们在需要真实环境时互补。

— Editorial Team

Advertisement 728x90

继续阅读