3. 项目结构设计:标准 Go 项目布局、审计网关模块划分、测试目录规范

在编写单测夹具之前,必须先确立清晰的项目骨架。一个结构混乱的项目会导致测试难以定位、夹具难以复用、依赖难以注入。本章将围绕 标准 Go 项目布局、审计网关的模块划分 以及 测试目录(_test.go)规范 三个核心维度,构建出既符合社区惯例又适配单测夹具实战的项目结构。

3.1 标准 Go 项目布局

Go 官方并未强制规定项目布局,但社区经过多年实践形成了以 cmd/、internal/、pkg/ 为核心的布局模式。本课程采用该模式,并针对审计网关场景做适当裁剪。

目录 / 文件 用途 单测夹具关注点
cmd/audit-gateway/ 主入口,包含 main.go 不直接写单测,但需确保依赖可注入
internal/ 私有应用逻辑,禁止外部导入 夹具主要放置区,internal/ 下的 _test.go 可访问内部符号
pkg/ 可对外导出的公共库 夹具需 mock 外部依赖时使用
api/ API 定义(protobuf / OpenAPI) 生成 mock 桩代码的输入源
config/ 配置文件与加载逻辑 夹具中构造测试配置的模板
scripts/ 构建、部署、迁移脚本 可包含夹具数据生成脚本
test/ 集成测试与端到端测试 存放跨模块的夹具编排
go.mod / go.sum 依赖管理 确保测试依赖与主模块一致
关键原则: internal/ 是单测夹具的主战场。因为 internal 包只能被其父目录下的代码导入,这天然隔离了测试代码与外部使用者,避免夹具污染公共 API。

3.2 审计网关模块划分

审计网关的核心职责是:拦截请求 → 提取审计上下文 → 执行策略判定 → 记录审计日志。我们将这些职责拆分为以下模块,每个模块对应 internal/ 下的一个子包:

  • interceptor:HTTP/gRPC 拦截器,负责从请求中提取元数据。
  • context:审计上下文构建,包含用户、资源、动作、环境等信息。
  • policy:策略引擎,判定当前请求是否允许通过、是否需要告警。
  • logger:审计日志结构化输出,支持多种后端(文件、Kafka、数据库)。
  • store:审计记录持久化存储接口与实现。
  • config:模块级配置加载与校验。

下图展示了模块间的依赖关系与数据流向:

审计网关模块划分与数据流向 审计网关模块划分与数据流向 外部请求 interceptor context policy logger store 模块依赖说明 interceptor 依赖 context context 依赖 config policy 依赖 context + config logger 依赖 context + store
模块划分原则: 每个模块只做一件事,且通过接口依赖下游模块。例如 policy 不直接调用 store,而是通过 logger 接口间接记录结果。这种设计让单测夹具可以轻松替换真实实现为 mock 或 fake。

3.3 测试目录(_test.go)规范

Go 的测试工具链通过 _test.go 后缀识别测试文件。在审计网关项目中,我们进一步细化规范,确保夹具代码与测试逻辑分离、可复用、可维护。

3.3.1 测试文件命名与位置

文件命名模式 放置位置 用途
*_test.go 与被测包同级目录 单元测试,白盒测试,可访问包内未导出符号
*_test.go + package xxx_test 与被测包同级目录 外部测试(黑盒),仅测试导出 API
fixture_*.go internal/testfixture/ 夹具定义与构造器,不包含测试逻辑
mock_*.go internal/mock/ 由 mock 生成器(如 mockgen)自动生成
避坑: 不要将夹具代码直接写在 *_test.go 中。如果多个测试文件需要相同的夹具,会导致重复定义。正确的做法是将夹具提取到 internal/testfixture/ 包中,测试文件通过导入该包来使用夹具。

3.3.2 夹具目录结构示例

以下是一个符合规范的夹具目录结构:

internal/
├── interceptor/
│   ├── interceptor.go
│   ├── interceptor_test.go        # 单元测试
│   └── fixture_interceptor.go     # 本包专用的简单夹具(可选)
├── policy/
│   ├── policy.go
│   ├── policy_test.go
│   └── policy_test.go             # 外部测试(黑盒)
├── testfixture/                   # 全局夹具仓库
│   ├── fixture_context.go         # 构造审计上下文的夹具
│   ├── fixture_policy_rule.go     # 构造策略规则的夹具
│   └── fixture_log_entry.go       # 构造审计日志条目的夹具
├── mock/                          # 自动生成的 mock
│   ├── mock_interceptor.go
│   ├── mock_policy.go
│   └── mock_logger.go
└── store/
    ├── store.go
    └── store_test.go

3.3.3 夹具函数命名规范

夹具函数应遵循 New[被测类型]ForTest 或 Build[被测类型] 模式,并明确返回类型和错误:

// internal/testfixture/fixture_context.go

package testfixture

import (
    "time"
    "audit-gateway/internal/context"
)

// NewAuditContextForTest 构造一个用于测试的审计上下文。
// 参数 opts 允许调用方覆盖默认字段值。
func NewAuditContextForTest(opts ...AuditContextOption) (*context.AuditContext, error) {
    // 默认值
    ctx := &context.AuditContext{
        UserID:    "test-user-001",
        Resource:  "/api/v1/orders",
        Action:    "READ",
        Timestamp: time.Now().UTC(),
        SourceIP:  "192.168.1.100",
    }
    // 应用自定义选项
    for _, opt := range opts {
        if err := opt(ctx); err != nil {
            return nil, err
        }
    }
    return ctx, nil
}

// AuditContextOption 是修改审计上下文的函数式选项。
type AuditContextOption func(*context.AuditContext) error
推荐模式: 使用函数式选项(Functional Options)模式构造夹具。它允许测试用例只覆盖关心的字段,其余字段使用合理的默认值,大幅减少测试代码量。

3.3.4 测试文件内部组织

每个 _test.go 文件应遵循以下结构:

  1. 包声明:白盒测试使用 package xxx,黑盒测试使用 package xxx_test。
  2. 导入块:按标准库、第三方库、内部包分组。
  3. TestMain(可选):用于全局的 setup / teardown。
  4. 测试函数:每个函数对应一个测试场景,使用 t.Run 组织子测试。
  5. 辅助函数:仅本文件使用的辅助函数,以小写字母开头。
// internal/policy/policy_test.go
package policy

import (
    "testing"
    "audit-gateway/internal/testfixture"
)

func TestEvaluate_Allow(t *testing.T) {
    ctx, err := testfixture.NewAuditContextForTest()
    if err != nil {
        t.Fatalf("failed to create fixture: %v", err)
    }

    p := NewPolicy(testfixture.NewAllowAllRules())
    result := p.Evaluate(ctx)

    if result != Allow {
        t.Errorf("expected Allow, got %v", result)
    }
}

func TestEvaluate_Deny(t *testing.T) {
    ctx, err := testfixture.NewAuditContextForTest(
        testfixture.WithResource("/api/v1/admin"),
    )
    if err != nil {
        t.Fatalf("failed to create fixture: %v", err)
    }

    p := NewPolicy(testfixture.NewDenyAdminRules())
    result := p.Evaluate(ctx)

    if result != Deny {
        t.Errorf("expected Deny, got %v", result)
    }
}

3.4 本章小结

  • 采用 cmd/ + internal/ + pkg/ 的标准 Go 项目布局,internal/ 是单测夹具的核心存放区。
  • 审计网关按职责拆分为 interceptor、context、policy、logger、store、config 六个模块,模块间通过接口依赖,便于测试替身注入。
  • 测试目录规范要求:夹具代码与测试逻辑分离,统一放在 internal/testfixture/;mock 代码放在 internal/mock/;测试文件使用函数式选项模式构造夹具,提高可读性和复用性。