# Zenfeed 最新测试策略与风格 > 适用版本:v0.2.2 ## 1. 引言 Zenfeed 的测试策略核心目标是: * **清晰性 (Clarity)**:测试本身应如文档般易于理解,清晰地表达被测功能的行为和预期。 * **可信性 (Reliability)**:测试结果应准确反映代码的健康状况,确保每次提交的信心。 * **可维护性 (Maintainability)**:测试代码应易于修改和扩展,以适应项目的持续演进。 本指南旨在详细介绍 Zenfeed 项目所遵循的测试理念、风格和具体实践。 ## 2. 核心测试理念与风格 Zenfeed 的测试方法论深受行为驱动开发 (BDD) 的影响,并结合了表驱动测试等高效实践。 ### 2.1 行为驱动开发 我们选择 BDD 作为核心的测试描述框架,主要基于以下原因(其理念也体现在 `pkg/test/test.go` 的 `Case` 结构设计中): * **提升可读性 (Enhanced Readability)**:BDD 强调使用自然语言描述软件的行为。每个测试用例读起来都像一个用户故事或一个功能说明,这使得测试本身就成为了一种精确的"活文档"。 * **关注行为 (Focus on Behavior)**:测试不再仅仅是验证代码片段的输入输出,而是从模块、组件或用户交互的层面描述其应有的行为。这有助于确保我们构建的功能符合预期。 * **需求驱动 (Requirement-Driven)**:测试直接对应需求描述,而非实现细节。这种自顶向下的方法确保了测试的稳定性,即使内部实现重构,只要行为不变,测试依然有效。 BDD 通常使用 `Scenario`, `Given`, `When`, `Then` 的结构来组织测试: * **`Scenario` (场景)**:描述测试用例所针对的特性或功能点。 * 例如:`"Query hot block with label filters"` (查询带标签过滤的热数据块) * **`Given` (给定)**:描述场景开始前的初始上下文或状态(**注意:这不是指方法的输入参数**)。 * 例如:`"a hot block with indexed feeds"` (一个已索引了 Feed 的热数据块) * **`When` (当)**:描述触发场景的事件或操作(**这部分通常包含被测方法的输入参数**)。 * 例如:`"querying with label filters"` (当使用标签过滤器进行查询时) * **`Then` (那么)**:描述场景结束后预期的结果或状态变化。 * 例如:`"should return matching feeds"` (那么应该返回匹配的 Feed) 为了更好地在代码中实践 BDD,我们定义了 `pkg/test/test.go` 中的 `Case[GivenDetail, WhenDetail, ThenExpected]` 泛型结构。其中: * `GivenDetail`: 存储 `Given` 子句描述的初始状态的具体数据。 * `WhenDetail`: 存储 `When` 子句描述的事件或方法调用的具体参数。 * `ThenExpected`: 存储 `Then` 子句描述的预期结果。 这种结构化不仅增强了测试数据的类型安全,也使得测试用例的意图更加明确。对于需要模拟依赖项的组件,`GivenDetail` 通常会包含用于配置这些模拟行为的 `component.MockOption`,我们将在后续 Mocking 章节详细讨论。 ### 2.2 表驱动测试 当一个功能或方法需要针对多种不同的输入组合、边界条件或状态进行测试时,表驱动测试是一种非常高效和整洁的组织方式。 * **简洁性 (Conciseness)**:将所有测试用例的数据(输入、参数、预期输出)集中定义在一个表格(通常是切片)中,避免了为每个 case编写大量重复的测试逻辑。 * **易扩展性 (Extensibility)**:添加新的测试场景变得非常简单,只需在表格中增加一条新记录即可。 * **清晰性 (Clarity)**:所有相关的测试用例一目了然,便于快速理解被测功能的覆盖范围。 **实践约定**: 在 Zenfeed 中,**当存在多个测试用例时,必须使用表驱动测试**。 ### 2.3 测试结构约定 为了保持项目范围内测试代码的一致性和可读性,我们约定在测试文件中遵循以下组织结构: 1. **定义辅助类型 (Define Helper Types)**:在测试函数的开头部分,通常会为 `GivenDetail`, `WhenDetail`, `ThenExpected` 定义具体的结构体类型,以增强类型安全和表达力。 2. **定义测试用例表 (Define Test Case Table)**:将所有测试用例集中定义在一个 `[]test.Case` 类型的切片中。 3. **循环执行测试 (Loop Through Test Cases)**:使用 `for` 循环遍历测试用例表,并为每个用例运行 `t.Run(tt.Scenario, func(t *testing.T) { ... })`。 4. **清晰的 G/W/T 逻辑块 (Clear G/W/T Blocks)**:在每个 `t.Run` 的匿名函数内部,根据需要组织代码块,以对应 `Given`(准备初始状态,通常基于 `tt.GivenDetail`),`When`(执行被测操作,通常使用 `tt.WhenDetail`),和 `Then`(断言结果,通常对比 `tt.ThenExpected`)。 5. **描述性变量名 (Descriptive Variable Names)**:使用与 BDD 术语(如 `given`, `when`, `then`, `expected`, `actual`)相匹配或能清晰表达意图的变量名。 ## 3. 依赖隔离:Mocking (Dependency Isolation: Mocking) 单元测试的核心原则之一是**隔离性 (Isolation)**,即被测试的代码单元(如一个函数或一个方法)应该与其依赖项隔离开来。Mocking (模拟) 是实现这种隔离的关键技术。 我们主要使用 `github.com/stretchr/testify/mock` 库来实现 Mocking。特别是对于实现了 `pkg/component/component.go` 中 `Component` 接口的组件,我们提供了一种标准的 Mocking 方式。 ```go type givenDetail struct { // Example of another initial state field for the component being tested initialProcessingPrefix string // MockOption to set up the behavior of dependencyA dependencyAMockSetup component.MockOption // ... } type whenDetail struct { processDataInput string // ... } type thenExpected struct { expectedOutput string expectedError error // ... } tests := []test.Case[givenDetail, whenDetail, thenExpected]{ { Scenario: "Component processes data successfully with mocked dependency", Given: "YourComponent with an initial prefix and dependencyA mocked to return 'related_data_value' for 'input_key'", When: "ProcessData is called with 'input_key'", Then: "Should return 'prefix:input_key:related_data_value' and no error", GivenDetail: givenDetail{ initialProcessingPrefix: "prefix1", dependencyAMockSetup: func(m *mock.Mock) { // We expect DependencyA's FetchRelatedData to be called with "input_key" // and it should return "related_data_value" and no error. m.On("FetchRelatedData", "input_key"). Return("related_data_value", nil). Once() // Expect it to be called exactly once. }, }, WhenDetail: whenDetail{ processDataInput: "input_key", }, ThenExpected: thenExpected{ expectedOutput: "prefix1:input_key:related_data_value", expectedError: nil, }, }, // ...更多测试用例... } // 在 for _, tt := range tests { t.Run(tt.Scenario, func(t *testing.T) { ... }) } 循环内部 // Given 阶段: Setup mocks and the component under test var mockHelperForDepA *mock.Mock defer func() { // 确保在每个子测试结束时断言 if mockHelperForDepA != nil { mockHelperForDepA.AssertExpectations(t) } }() // 创建并配置 mockDependencyA // dependency_a_pkg.NewFactory 应该是一个返回 DependencyA 接口和 error 的工厂函数 // 它接受 component.MockOption 来配置其内部的 mock.Mock 对象 mockDependencyA, err := dependency_a_pkg.NewFactory( component.MockOption(func(m *mock.Mock) { mockHelperForDepA = m // 保存 mock.Mock 实例以供 AssertExpectations 使用 if tt.GivenDetail.dependencyAMockSetup != nil { // 应用测试用例中定义的 specific mock setup tt.GivenDetail.dependencyAMockSetup(m) } }), ).New("mocked_dep_a_instance", nil /* config for dep A */, dependency_a_pkg.Dependencies{}) Expect(err).NotTo(HaveOccurred()) Expect(mockDependencyA).NotTo(BeNil()) // 假设 YourComponent 的构造函数如下: componentUnderTest := NewYourComponent(tt.GivenDetail.initialProcessingPrefix, mockDependencyA) // When 阶段: Execute the action being tested actualOutput, actualErr := componentUnderTest.ProcessData(context.Background(), tt.WhenDetail.processDataInput) // Then 阶段: Assert the outcomes if tt.ThenExpected.expectedError != nil { Expect(actualErr).To(HaveOccurred()) Expect(actualErr.Error()).To(Equal(tt.ThenExpected.expectedError.Error())) } else { Expect(actualErr).NotTo(HaveOccurred()) } Expect(actualOutput).To(Equal(tt.ThenExpected.expectedOutput)) ```