编写控制器测试

测试 Kubernetes 控制器是一个庞大的主题,而 kubebuilder 为您生成的样板测试文件相对较少。

为了引导您了解 Kubebuilder 生成的控制器的集成测试模式,我们将回顾我们在第一个教程中构建的 CronJob,并为其编写一个简单的测试。

基本方法是,在生成的 suite_test.go 文件中,您将使用 envtest 创建一个本地 Kubernetes API 服务器,实例化和运行您的控制器,然后编写额外的 *_test.go 文件使用 Ginkgo 进行测试。

如果您想调整您的 envtest 集群的配置,请参阅 为集成测试配置 envtest 部分以及 envtest 文档

测试环境设置

../../cronjob-tutorial/testdata/project/internal/controller/suite_test.go
Apache License

版权所有 2024 年 Kubernetes 作者。

根据 Apache 许可证 2.0 版(“许可证”)许可; 除非符合许可证的规定,否则您不得使用此文件。 您可以在以下网址获取许可证的副本:

http://www.apache.org/licenses/LICENSE-2.0

除非适用法律要求或经书面同意,否则根据许可证分发的软件 按“原样“提供,不附带任何担保或条件,无论是明示的还是暗示的。 请查看许可证以了解特定语言下的权限和限制。

Imports

当我们在上一章中使用 kubebuilder create api 创建 CronJob API 时,Kubebuilder 已经为您做了一些测试工作。 Kubebuilder 生成了一个 internal/controller/suite_test.go 文件,其中包含了设置测试环境的基本内容。

首先,它将包含必要的导入项。

package controller

// 这些测试使用 Ginkgo(BDD 风格的 Go 测试框架)。请参考
// http://onsi.github.io/ginkgo/ 了解更多关于 Ginkgo 的信息。

现在,让我们来看一下生成的代码。

var (
    cfg       *rest.Config
    k8sClient client.Client // 您将在测试中使用此客户端。
    testEnv   *envtest.Environment
    ctx       context.Context
    cancel    context.CancelFunc
)

func TestControllers(t *testing.T) {
    RegisterFailHandler(Fail)

    RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
    // 省略了一些设置代码
})

Kubebuilder 还生成了用于清理 envtest 并在控制器目录中实际运行测试文件的样板函数。 您不需要修改这些函数。

var _ = AfterSuite(func() {
    // 省略了一些清理代码
})

现在,您的控制器在测试集群上运行,并且已准备好在您的 CronJob 上执行操作的客户端,我们可以开始编写集成测试了!

测试控制器行为

../../cronjob-tutorial/testdata/project/internal/controller/cronjob_controller_test.go
Apache License

根据 Apache 许可证 2.0 版(“许可证”)许可; 除非符合许可证的规定,否则您不得使用此文件。 您可以在以下网址获取许可证的副本:

http://www.apache.org/licenses/LICENSE-2.0

除非适用法律要求或经书面同意,根据许可证分发的软件 按“原样“提供,不附带任何担保或条件,无论是明示的还是暗示的。 请查看许可证以了解特定语言下的权限和限制。

理想情况下,对于每个在 suite_test.go 中调用的控制器,我们应该有一个 <kind>_controller_test.go。 因此,让我们为 CronJob 控制器编写示例测试(cronjob_controller_test.go)。

Imports

和往常一样,我们从必要的导入项开始。我们还定义了一些实用变量。

package controller

import (
	"context"
	"reflect"
	"time"

	batchv1 "k8s.io/api/batch/v1"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"

	cronjobv1 "tutorial.kubebuilder.io/project/api/v1"
)

编写简单集成测试的第一步是实际创建一个 CronJob 实例,以便对其运行测试。 请注意,要创建 CronJob,您需要创建一个包含您的 CronJob 规范的存根 CronJob 结构。

请注意,当我们创建存根 CronJob 时,CronJob 还需要其所需的下游对象的存根。 如果没有下游的存根 Job 模板规范和下游的 Pod 模板规范,Kubernetes API 将无法创建 CronJob。

var _ = Describe("CronJob controller", func() {

    // 为对象名称和测试超时/持续时间和间隔定义实用常量。
    const (
        CronjobName      = "test-cronjob"
        CronjobNamespace = "default"
        JobName          = "test-job"

        timeout  = time.Second * 10
        duration = time.Second * 10
        interval = time.Millisecond * 250
    )

    Context("当更新 CronJob 状态时", func() {
        It("当创建新的 Job 时,应增加 CronJob 的 Status.Active 计数", func() {
            By("创建一个新的 CronJob")
            ctx := context.Background()
            cronJob := &cronjobv1.CronJob{
                TypeMeta: metav1.TypeMeta{
                    APIVersion: "batch.tutorial.kubebuilder.io/v1",
                    Kind:       "CronJob",
                },
                ObjectMeta: metav1.ObjectMeta{
                    Name:      CronjobName,
                    Namespace: CronjobNamespace,
                },
                Spec: cronjobv1.CronJobSpec{
                    Schedule: "1 * * * *",
                    JobTemplate: batchv1.JobTemplateSpec{
                        Spec: batchv1.JobSpec{
                            // 为简单起见,我们只填写了必填字段。
                            Template: v1.PodTemplateSpec{
                                Spec: v1.PodSpec{
                                    // 为简单起见,我们只填写了必填字段。
                                    Containers: []v1.Container{
                                        {
                                            Name:  "test-container",
                                            Image: "test-image",
                                        },
                                    },
                                    RestartPolicy: v1.RestartPolicyOnFailure,
                                },
                            },
                        },
                    },
                },
            }
            Expect(k8sClient.Create(ctx, cronJob)).Should(Succeed())

           

创建完这个 CronJob 后,让我们检查 CronJob 的 Spec 字段是否与我们传入的值匹配。 请注意,由于 k8s apiserver 在我们之前的 Create() 调用后可能尚未完成创建 CronJob,我们将使用 Gomega 的 Eventually() 测试函数,而不是 Expect(),以便让 apiserver 有机会完成创建我们的 CronJob。

Eventually() 将重复运行作为参数提供的函数,直到 (a) 函数的输出与随后的 Should() 调用中的预期值匹配,或者 (b) 尝试次数 * 间隔时间超过提供的超时值。

在下面的示例中,timeout 和 interval 是我们选择的 Go Duration 值。

            cronjobLookupKey := types.NamespacedName{Name: CronjobName, Namespace: CronjobNamespace}
            createdCronjob := &cronjobv1.CronJob{}

            // 我们需要重试获取这个新创建的 CronJob,因为创建可能不会立即发生。
            Eventually(func() bool {
                err := k8sClient.Get(ctx, cronjobLookupKey, createdCronjob)
                return err == nil
            }, timeout, interval).Should(BeTrue())
            // 让我们确保我们的 Schedule 字符串值被正确转换/处理。
            Expect(createdCronjob.Spec.Schedule).Should(Equal("1 * * * *"))
           

现在我们在测试集群中创建了一个 CronJob,下一步是编写一个测试,实际测试我们的 CronJob 控制器的行为。 让我们测试负责更新 CronJob.Status.Active 以包含正在运行的 Job 的 CronJob 控制器逻辑。 我们将验证当 CronJob 有一个活动的下游 Job 时,其 CronJob.Status.Active 字段包含对该 Job 的引用。

首先,我们应该获取之前创建的测试 CronJob,并验证它当前是否没有任何活动的 Job。 我们在这里使用 Gomega 的 Consistently() 检查,以确保在一段时间内活动的 Job 计数保持为 0。

            By("检查 CronJob 是否没有活动的 Jobs")
            Consistently(func() (int, error) {
                err := k8sClient.Get(ctx, cronjobLookupKey, createdCronjob)
                if err != nil {
                    return -1, err
                }
                return len(createdCronjob.Status.Active), nil
            }, duration, interval).Should(Equal(0))
           
接下来,我们实际创建一个属于我们的 CronJob 的存根 Job,以及其下游模板规范。
我们将 Job 的状态的 "Active" 计数设置为 2,以模拟 Job 运行两个 Pod,这意味着 Job 正在活动运行。

然后,我们获取存根 Job,并将其所有者引用设置为指向我们的测试 CronJob。
这确保测试 Job 属于我们的测试 CronJob,并由其跟踪。

完成后,我们创建我们的新 Job 实例。

            By("创建一个新的 Job")
            testJob := &batchv1.Job{
                ObjectMeta: metav1.ObjectMeta{
                    Name:      JobName,
                    Namespace: CronjobNamespace,
                },
                Spec: batchv1.JobSpec{
                    Template: v1.PodTemplateSpec{
                        Spec: v1.PodSpec{
                            // 为简单起见,我们只填写了必填字段。
                            Containers: []v1.Container{
                                {
                                    Name:  "test-container",
                                    Image: "test-image",
                                },
                            },
                            RestartPolicy: v1.RestartPolicyOnFailure,
                        },
                    },
                },
                Status: batchv1.JobStatus{
                    Active: 2,
                },
            }

            // 请注意,设置此所有者引用需要您的 CronJob 的 GroupVersionKind。
            kind := reflect.TypeOf(cronjobv1.CronJob{}).Name()
            gvk := cronjobv1.GroupVersion.WithKind(kind)

            controllerRef := metav1.NewControllerRef(createdCronjob, gvk)
            testJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef})
            Expect(k8sClient.Create(ctx, testJob)).Should(Succeed())
           
将此 Job 添加到我们的测试 CronJob 应该触发我们控制器的协调逻辑。

之后,我们可以编写一个测试,评估我们的控制器是否最终按预期更新我们的 CronJob 的 Status 字段!

            By("检查 CronJob 是否有一个活动的 Job")
            Eventually(func() ([]string, error) {
                err := k8sClient.Get(ctx, cronjobLookupKey, createdCronjob)
                if err != nil {
                    return nil, err
                }

                names := []string{}
                for _, job := range createdCronjob.Status.Active {
                    names = append(names, job.Name)
                }
                return names, nil
            }, timeout, interval).Should(ConsistOf(JobName), "应在状态的活动作业列表中列出我们的活动作业 %s", JobName)
        })
    })

})

编写完所有这些代码后,您可以再次在您的 controllers/ 目录中运行 go test ./... 来运行您的新测试!

上面的状态更新示例演示了一个用于自定义 Kind 与下游对象的一般测试策略。到目前为止,您希望已经学会了以下测试控制器行为的方法:

  • 设置您的控制器在 envtest 集群上运行
  • 编写用于创建测试对象的存根
  • 隔离对象的更改以测试特定的控制器行为

高级示例

有更复杂的示例使用 envtest 严格测试控制器行为。示例包括:

  • Azure Databricks Operator:查看他们完全完善的 suite_test.go 以及该目录中的任何 *_test.go 文件 比如这个