ginkgo BeforeAll vs BeforeEach

20 阅读4分钟

BeforeAll vs f.BeforeEach() 设计与使用场景

设计场景对比

1. Ginkgo 的 BeforeAll vs BeforeEach

// Ginkgo 生命周期钩子
ginkgo.BeforeAll(func() {
    // 在整个 Describe/Context 块的所有测试开始前执行一次
    // 用于:创建共享的昂贵资源(docker 网络、数据库等)
})

ginkgo.BeforeEach(func() {
    // 在每个 It 测试用例前执行
    // 用于:为每个测试准备隔离的环境(创建 namespace、pod 等)
})

2. Framework 的 f.BeforeEach() 方法

这是 kube-ovn e2e framework 的初始化方法,负责:

  • 惰性创建各种 Kubernetes 客户端(f.KubeOVNClientSetf.AttachNetClient 等)
  • 使用 if client == nil 检查,确保客户端只初始化一次
  • 虽然命名为 BeforeEach(),但可以在任何地方手动调用

执行顺序

典型场景(如 subnet 测试):

var _ = framework.Describe("[group:subnet]", func() {
    f := framework.NewDefaultFramework("subnet")
    
    // ❌ 此时 f.ClientSet 等客户端都是 nil
    
    ginkgo.BeforeEach(func() {
        // ✅ Ginkgo 自动调用每个测试前
        // 通常在这里访问 f.ClientSet(会触发 framework 自动初始化)
        cs = f.ClientSet  // framework.Framework 内部懒加载
        podClient = f.PodClient()
    })
    
    ginkgo.It("should do something", func() {
        // 测试代码
    })
})

你们当前的场景(使用 BeforeAll + 显式调用):

var _ = framework.OrderedDescribe("[group:iptables-vpc-nat-gw]", func() {
    f := framework.NewDefaultFramework("iptables-vpc-nat-gw")
    
    ginkgo.BeforeAll(func() {
        // ⚠<fe0f> 问题:BeforeAll 在所有测试前执行
        // 但 framework 客户端此时还未初始化
        
        // ✅ 解决方案:手动调用 f.BeforeEach() 初始化客户端
        f.BeforeEach()  
        
        // ✅ 现在可以安全访问客户端
        cs = f.ClientSet
        attachNetClient = f.NetworkAttachmentDefinitionClientNS(...)
    })
})

执行时序图

测试启动
  ↓
[OrderedDescribe 块开始]
  ↓
ginkgo.BeforeAll()  ← 只执行 1 次
  ├─ f.BeforeEach() ← 手动调用,初始化所有客户端
  ├─ 创建 docker network(共享资源)
  └─ 连接节点到网络
  ↓
[第 1 个 It 测试][第 2 个 It 测试]
  ↓
...
  ↓
ginkgo.AfterAll()   ← 只执行 1 次
  └─ 清理共享资源

为什么需要在 BeforeAll 中调用 f.BeforeEach()?

框架设计的默认假设:

  • Framework 期望在 ginkgo.BeforeEach 钩子中首次访问客户端
  • Kubernetes e2e framework 会在首次访问 f.ClientSet 时自动初始化

你们的特殊需求:

  • 使用 ginkgo.Ordered + BeforeAll 共享 docker 网络等昂贵资源
  • 需要在 BeforeAll 中访问客户端(比原设计更早)
  • 因此必须手动调用 f.BeforeEach() 触发初始化

最佳实践建议

ginkgo.BeforeAll(func() {
    // 1<fe0f><20e3> 首先初始化 framework
    f.BeforeEach()
    
    // 2<fe0f><20e3> 然后初始化测试客户端
    cs = f.ClientSet
    attachNetClient = f.NetworkAttachmentDefinitionClientNS(...)
    
    // 3<fe0f><20e3> 最后创建共享资源
    network, err := docker.NetworkCreate(...)
    ginkgo.DeferCleanup(func() {
        docker.NetworkDelete(network.ID)
    })
})

关键要点

钩子执行时机用途是否需要手动调用
ginkgo.BeforeAll所有测试前 1 次创建共享资源❌ 由 Ginkgo 管理
ginkgo.BeforeEach每个测试前隔离环境准备❌ 由 Ginkgo 管理
f.BeforeEach()手动/自动初始化 framework 客户端⚠ 在 BeforeAll 中需要手动调用
ginkgo.AfterAll所有测试后 1 次清理共享资源❌ 由 Ginkgo 管理

这样设计确保了客户端在使用前已正确初始化,避免了空指针错误 ✓ EOF

BeforeAll vs f.BeforeEach() 设计与使用场景

设计场景对比

1. Ginkgo 的 BeforeAll vs BeforeEach

// Ginkgo 生命周期钩子
ginkgo.BeforeAll(func() {
    // 在整个 Describe/Context 块的所有测试开始前执行一次
    // 用于:创建共享的昂贵资源(docker 网络、数据库等)
})

ginkgo.BeforeEach(func() {
    // 在每个 It 测试用例前执行
    // 用于:为每个测试准备隔离的环境(创建 namespace、pod 等)
})

2. Framework 的 f.BeforeEach() 方法

这是 kube-ovn e2e framework 的初始化方法,负责:

  • 惰性创建各种 Kubernetes 客户端(f.KubeOVNClientSetf.AttachNetClient 等)
  • 使用 if client == nil 检查,确保客户端只初始化一次
  • 虽然命名为 BeforeEach(),但可以在任何地方手动调用

执行顺序

典型场景(如 subnet 测试):

var _ = framework.Describe("[group:subnet]", func() {
    f := framework.NewDefaultFramework("subnet")
    
    // ❌ 此时 f.ClientSet 等客户端都是 nil
    
    ginkgo.BeforeEach(func() {
        // ✅ Ginkgo 自动调用每个测试前
        // 通常在这里访问 f.ClientSet(会触发 framework 自动初始化)
        cs = f.ClientSet  // framework.Framework 内部懒加载
        podClient = f.PodClient()
    })
    
    ginkgo.It("should do something", func() {
        // 测试代码
    })
})

你们当前的场景(使用 BeforeAll + 显式调用):

var _ = framework.OrderedDescribe("[group:iptables-vpc-nat-gw]", func() {
    f := framework.NewDefaultFramework("iptables-vpc-nat-gw")
    
    ginkgo.BeforeAll(func() {
        // ⚠️ 问题:BeforeAll 在所有测试前执行
        // 但 framework 客户端此时还未初始化
        
        // ✅ 解决方案:手动调用 f.BeforeEach() 初始化客户端
        f.BeforeEach()  
        
        // ✅ 现在可以安全访问客户端
        cs = f.ClientSet
        attachNetClient = f.NetworkAttachmentDefinitionClientNS(...)
    })
})

执行时序图

测试启动
  ↓
[OrderedDescribe 块开始]
  ↓
ginkgo.BeforeAll()  ← 只执行 1 次
  ├─ f.BeforeEach() ← 手动调用,初始化所有客户端
  ├─ 创建 docker network(共享资源)
  └─ 连接节点到网络
  ↓
[第 1 个 It 测试][第 2 个 It 测试]
  ↓
...
  ↓
ginkgo.AfterAll()   ← 只执行 1 次
  └─ 清理共享资源

为什么需要在 BeforeAll 中调用 f.BeforeEach()?

框架设计的默认假设:

  • Framework 期望在 ginkgo.BeforeEach 钩子中首次访问客户端
  • Kubernetes e2e framework 会在首次访问 f.ClientSet 时自动初始化

你们的特殊需求:

  • 使用 ginkgo.Ordered + BeforeAll 共享 docker 网络等昂贵资源
  • 需要在 BeforeAll 中访问客户端(比原设计更早)
  • 因此必须手动调用 f.BeforeEach() 触发初始化

最佳实践建议

ginkgo.BeforeAll(func() {
    // 1️⃣ 首先初始化 framework
    f.BeforeEach()
    
    // 2️⃣ 然后初始化测试客户端
    cs = f.ClientSet
    attachNetClient = f.NetworkAttachmentDefinitionClientNS(...)
    
    // 3️⃣ 最后创建共享资源
    network, err := docker.NetworkCreate(...)
    ginkgo.DeferCleanup(func() {
        docker.NetworkDelete(network.ID)
    })
})

关键要点

钩子执行时机用途是否需要手动调用
ginkgo.BeforeAll所有测试前 1 次创建共享资源❌ 由 Ginkgo 管理
ginkgo.BeforeEach每个测试前隔离环境准备❌ 由 Ginkgo 管理
f.BeforeEach()手动/自动初始化 framework 客户端⚠️ 在 BeforeAll 中需要手动调用
ginkgo.AfterAll所有测试后 1 次清理共享资源❌ 由 Ginkgo 管理

这样设计确保了客户端在使用前已正确初始化,避免了空指针错误 ✓