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:模块级配置加载与校验。
下图展示了模块间的依赖关系与数据流向:
模块划分原则: 每个模块只做一件事,且通过接口依赖下游模块。例如
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 文件应遵循以下结构:
- 包声明:白盒测试使用
package xxx,黑盒测试使用package xxx_test。 - 导入块:按标准库、第三方库、内部包分组。
- TestMain(可选):用于全局的 setup / teardown。
- 测试函数:每个函数对应一个测试场景,使用
t.Run组织子测试。 - 辅助函数:仅本文件使用的辅助函数,以小写字母开头。
// 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/;测试文件使用函数式选项模式构造夹具,提高可读性和复用性。