{"id":13694132,"url":"https://github.com/caicloud/learning-unit-testing-for-k8s-operator","last_synced_at":"2025-04-19T18:23:40.084Z","repository":{"id":75261719,"uuid":"201176103","full_name":"caicloud/learning-unit-testing-for-k8s-operator","owner":"caicloud","description":"学习如何为 Kubernetes Operators 进行单元测试 Learning How to Write Unit Tests for Kubernetes Operators","archived":false,"fork":false,"pushed_at":"2019-08-13T09:15:46.000Z","size":4554,"stargazers_count":57,"open_issues_count":3,"forks_count":11,"subscribers_count":10,"default_branch":"master","last_synced_at":"2025-03-29T11:34:40.850Z","etag":null,"topics":["kubernetes","operator","unit-testing"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/caicloud.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2019-08-08T04:09:02.000Z","updated_at":"2024-12-30T23:11:00.000Z","dependencies_parsed_at":"2024-01-14T19:18:15.545Z","dependency_job_id":null,"html_url":"https://github.com/caicloud/learning-unit-testing-for-k8s-operator","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/caicloud%2Flearning-unit-testing-for-k8s-operator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/caicloud%2Flearning-unit-testing-for-k8s-operator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/caicloud%2Flearning-unit-testing-for-k8s-operator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/caicloud%2Flearning-unit-testing-for-k8s-operator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/caicloud","download_url":"https://codeload.github.com/caicloud/learning-unit-testing-for-k8s-operator/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249762223,"owners_count":21321896,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["kubernetes","operator","unit-testing"],"created_at":"2024-08-02T17:01:25.097Z","updated_at":"2025-04-19T18:23:40.029Z","avatar_url":"https://github.com/caicloud.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# learning-unit-testing-for-k8s-operator\n\n这一 Repo 旨在帮助 Kubernetes Operators 的开发者们学习如何为 Operators 实现单元测试。其中包括：\n\n- 为原生实现的 Operator 实现单元测试\n- 为 kubebuilder v1 生成的 Operator 实现单元测试\n- 为 kubebuilder v2 生成的 Operator 实现单元测试\n\n因此这一文档的受众是 Operator 开发者们，文档中为不同的实现方式（kubebuilder v1, v2, 原生实现）设计了不同的实验，配合实验阅读味道更佳。\n\nTable of Contents\n=================\n\n   * [learning-unit-testing-for-k8s-operator](#learning-unit-testing-for-k8s-operator)\n      * [为原生实现的 Operator 实现单元测试](#为原生实现的-operator-实现单元测试)\n         * [事先需要了解的知识](#事先需要了解的知识)\n         * [准备工作](#准备工作)\n         * [Operator 实现分析](#operator-实现分析)\n            * [Operator 的初始化](#operator-的初始化)\n            * [Sync 过程](#sync-过程)\n            * [单元测试](#单元测试)\n         * [Lab 1 实现单元测试](#lab-1-实现单元测试)\n            * [问题](#问题)\n            * [参考实现](#参考实现)\n         * [Lab 2 扩展内容：Table Driven Test](#lab-2-扩展内容table-driven-test)\n            * [背景知识](#背景知识)\n            * [问题](#问题-1)\n            * [参考实现](#参考实现-1)\n      * [为 kubebuilder v1 生成的 Operator 实现单元测试（TODO）](#为-kubebuilder-v1-生成的-operator-实现单元测试todo)\n      * [为 kubebuilder v2 生成的 Operator 实现单元测试（TODO）](#为-kubebuilder-v2-生成的-operator-实现单元测试todo)\n\nCreated by [gh-md-toc](https://github.com/ekalinin/github-markdown-toc)\n\n## 为原生实现的 Operator 实现单元测试\n\n原生实现的 Operator 实现单元测试的讲解与动手实验，是利用 [kubernetes/sample-controller a52d0d8](https://github.com/kubernetes/sample-controller/commit/a52d0d8c67c5addd613ec9082ed402f7f7c6579f) 作为示例展开的，为了实现动手实验的目的，修改了其单元测试 `controller_test.go` 中的内容。\n\n### 事先需要了解的知识\n\n- Kubernetes CRD 特性\n- Kubernetes Informer 机制\n- Golang 单元测试机制\n\n### 准备工作\n\n首先，将 `native-demo-operator` 复制到 `$GOPATH/src/k8s.io/sample-controller`。\n\n```sh\n# 将 `native-demo-operator` 复制到 `$GOPATH/src/k8s.io/sample-controller`。\n./scripts/install-native-operator.sh\n# 到 `$GOPATH/src/github.com/caicloud/kbv2-operator` 目录下\ncd $GOPATH/src/k8s.io/sample-controller\n```\n\n这一操作是为了确保 operator 在正确的路径下。此时已经准备好了 Operator 的环境。\n\n### Operator 实现分析\n\n注：如果已经熟悉 [kubernetes/sample-controller](https://github.com/kubernetes/sample-controller) 的实现与自带的单元测试，可跳过这一部分。\n\n原生实现的 Operator 实现了一个新的资源类型，Foo。Foo 的定义如下，它进一步抽象了 Deployment，只保留了 Deployment Name 和 Replicas 两个字段。在创建 Foo 时，Foo 会创建出以 Deployment Name 命名的 Deployment。而在 Foo 的状态中，只会显示目前 Foo 创建的 Deployment 目前可用的 Replicas 的数量。\n\n```go\ntype Foo struct {\n\tmetav1.TypeMeta   `json:\",inline\"`\n\tmetav1.ObjectMeta `json:\"metadata,omitempty\"`\n\n\tSpec   FooSpec   `json:\"spec\"`\n\tStatus FooStatus `json:\"status\"`\n}\n\n// FooSpec is the spec for a Foo resource\ntype FooSpec struct {\n\tDeploymentName string `json:\"deploymentName\"`\n\tReplicas       *int32 `json:\"replicas\"`\n}\n\n// FooStatus is the status for a Foo resource\ntype FooStatus struct {\n\tAvailableReplicas int32 `json:\"availableReplicas\"`\n}\n```\n\n#### Operator 的初始化\n\n如下代码是 Foo 的 Operator 初始化的过程。Foo 依赖两个 Client 和两个 Informer：kubeClient（用来操作 Deployment 资源），exampleClient（用来操作 Foo 资源），Deployment Informer（用来订阅 apiserver 上关于 Deployment 的事件），Foo Informer（用来订阅 Foo 资源的事件）。\n\n```go\n\tkubeInformerFactory := kubeinformers.NewSharedInformerFactory(kubeClient, time.Second*30)\n\texampleInformerFactory := informers.NewSharedInformerFactory(exampleClient, time.Second*30)\n\n\tcontroller := NewController(kubeClient, exampleClient,\n\t\tkubeInformerFactory.Apps().V1().Deployments(),\n\t\texampleInformerFactory.Samplecontroller().V1alpha1().Foos())\n\n\t// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh)\n\t// Start method is non-blocking and runs all registered informers in a dedicated goroutine.\n\tkubeInformerFactory.Start(stopCh)\n\texampleInformerFactory.Start(stopCh)\n\n\tif err = controller.Run(2, stopCh); err != nil {\n\t\tklog.Fatalf(\"Error running controller: %s\", err.Error())\n\t}\n```\n\n#### Sync 过程\n\nFoo Operator 如同 Kubernetes 内部的 controller 一样，维护了一个 workqueue，并且利用 `syncHandler` 比对现实状态与期望状态的不同，从现实状态努力同步到期望状态。\n\nSync 过程如下所示，首先会得到或者创建出对应的 Deployment，然后判断 Deployment 的 Replicas 是否与 Foo 的定义一致，如果不一致，则更新 Deployment。最后，更新 Foo 的状态。\n\n\u003cdetails\u003e\n  \u003csummary\u003e点击此处查看 syncHandler 代码\u003c/summary\u003e\n\n```go\nfunc (c *Controller) syncHandler(key string) error {\n\t// Convert the namespace/name string into a distinct namespace and name\n\tnamespace, name, err := cache.SplitMetaNamespaceKey(key)\n\tif err != nil {\n\t\tutilruntime.HandleError(fmt.Errorf(\"invalid resource key: %s\", key))\n\t\treturn nil\n\t}\n\n\t// Get the Foo resource with this namespace/name\n\tfoo, err := c.foosLister.Foos(namespace).Get(name)\n\tif err != nil {\n\t\t// The Foo resource may no longer exist, in which case we stop\n\t\t// processing.\n\t\tif errors.IsNotFound(err) {\n\t\t\tutilruntime.HandleError(fmt.Errorf(\"foo '%s' in work queue no longer exists\", key))\n\t\t\treturn nil\n\t\t}\n\n\t\treturn err\n\t}\n\n\tdeploymentName := foo.Spec.DeploymentName\n\tif deploymentName == \"\" {\n\t\tutilruntime.HandleError(fmt.Errorf(\"%s: deployment name must be specified\", key))\n\t\treturn nil\n\t}\n\n\t// Get the deployment with the name specified in Foo.spec\n\tdeployment, err := c.deploymentsLister.Deployments(foo.Namespace).Get(deploymentName)\n\t// If the resource doesn't exist, we'll create it\n\tif errors.IsNotFound(err) {\n\t\tdeployment, err = c.kubeclientset.AppsV1().Deployments(foo.Namespace).Create(newDeployment(foo))\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// If the Deployment is not controlled by this Foo resource, we should log\n\t// a warning to the event recorder and ret\n\tif !metav1.IsControlledBy(deployment, foo) {\n\t\tmsg := fmt.Sprintf(MessageResourceExists, deployment.Name)\n\t\tc.recorder.Event(foo, corev1.EventTypeWarning, ErrResourceExists, msg)\n\t\treturn fmt.Errorf(msg)\n\t}\n\n\t// If this number of the replicas on the Foo resource is specified, and the\n\t// number does not equal the current desired replicas on the Deployment, we\n\t// should update the Deployment resource.\n\tif foo.Spec.Replicas != nil \u0026\u0026 *foo.Spec.Replicas != *deployment.Spec.Replicas {\n\t\tklog.V(4).Infof(\"Foo %s replicas: %d, deployment replicas: %d\", name, *foo.Spec.Replicas, *deployment.Spec.Replicas)\n\t\tdeployment, err = c.kubeclientset.AppsV1().Deployments(foo.Namespace).Update(newDeployment(foo))\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\t// Finally, we update the status block of the Foo resource to reflect the\n\t// current state of the world\n\terr = c.updateFooStatus(foo, deployment)\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.recorder.Event(foo, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced)\n\treturn nil\n}\n```\n\u003c/details\u003e\n\n#### 单元测试\n\nOperator 的实现依赖 clienset 和 informer，informer 用来订阅 apiserver 的事件，触发 operator 的同步操作。clientset 用来与 apiserver 交互，进行增删改查等操作。\n\n因此在进行单元测试时，需要把这两个依赖 fake 掉。为了在实现单元测试用例时，更方便地完成 Fake 的操作，Foo Operator 引入了一个专门用于测试的数据结构 `fixture`。\n\n首先，我们会介绍 `fixture` 的定义以及部分实现，接下来，会以一个测试用例作为示例，了解如何使用 `fixture` 简化测试用例的实现。\n\n##### fixture 结构\n\nfixture 的定义如下：\n\n```go\ntype fixture struct {\n\tt *testing.T\n\n\tclient     *fake.Clientset\n\tkubeclient *k8sfake.Clientset\n\t// Objects to put in the store.\n\tfooLister        []*samplecontroller.Foo\n\tdeploymentLister []*apps.Deployment\n\t// Actions expected to happen on the client.\n\tkubeactions []core.Action\n\tactions     []core.Action\n\t// Objects from here preloaded into NewSimpleFake.\n\tkubeobjects []runtime.Object\n\tobjects     []runtime.Object\n}\n```\n\nfixture 在测试中，代表的就是一个在运行的 Operator，其中 `client` 与 `kubeclient` 是 fake 的 client。\n\n`deploymentLister` 和 `fooLister` 会定义一系列 Deployment 和 Foo 实例，这些实例会被加入到 Informer 的 Indexer 中，以便发起 Sync 请求。\n\n`kubeobjects` 和 `objects` 是用来构建期望的测试数据的。它们中的对象，会被添加到 `kubeclient` 和 `client` 中：\n\n```go\n\tf := newFixture(t)\n\tf.client = fake.NewSimpleClientset(f.objects...)\n\tf.kubeclient = k8sfake.NewSimpleClientset(f.kubeobjects...)\n```\n\n`NewSimpleClientset` 的定义如下所示，它利用了一个非常简单的 object 跟踪机制，绕过了正常实现的 clienset 中的各种 validation 和 defaults。它会记录对跟着的 object 的增删改查操作。\n\n```go\n// NewSimpleClientset returns a clientset that will respond with the provided objects.\n// It's backed by a very simple object tracker that processes creates, updates and deletions as-is,\n// without applying any validations and/or defaults. It shouldn't be considered a replacement\n// for a real clientset and is mostly useful in simple unit tests.\nfunc NewSimpleClientset(objects ...runtime.Object) *Clientset {\n\to := testing.NewObjectTracker(scheme, codecs.UniversalDecoder())\n\tfor _, obj := range objects {\n\t\tif err := o.Add(obj); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tcs := \u0026Clientset{tracker: o}\n\tcs.discovery = \u0026fakediscovery.FakeDiscovery{Fake: \u0026cs.Fake}\n\tcs.AddReactor(\"*\", \"*\", testing.ObjectReaction(o))\n\tcs.AddWatchReactor(\"*\", func(action testing.Action) (handled bool, ret watch.Interface, err error) {\n\t\tgvr := action.GetResource()\n\t\tns := action.GetNamespace()\n\t\twatch, err := o.Watch(gvr, ns)\n\t\tif err != nil {\n\t\t\treturn false, nil, err\n\t\t}\n\t\treturn true, watch, nil\n\t})\n\n\treturn cs\n}\n```\n\n`kubeactions` 和 `actions` 是用来记录期望观测到的，作用在 `client` 与 `kubeclient` 上的调用。Action 的定义如下所示：\n\n```go\ntype Action interface {\n\tGetNamespace() string\n\tGetVerb() string\n\tGetResource() schema.GroupVersionResource\n\tGetSubresource() string\n\tMatches(verb, resource string) bool\n\n\t// DeepCopy is used to copy an action to avoid any risk of accidental mutation.  Most people never need to call this\n\t// because the invocation logic deep copies before calls to storage and reactors.\n\tDeepCopy() Action\n}\n\ntype GetAction interface {\n\tAction\n\tGetName() string\n}\n\ntype CreateAction interface {\n\tAction\n\tGetObject() runtime.Object\n}\n\ntype UpdateAction interface {\n\tAction\n\tGetObject() runtime.Object\n}\n// ...\n```\n\n一个 Action 实例描述的是发生在 `clientset` 上的一次调用，其中包括 GET 请求操作（`GetAction`），创建操作（`CreateAction`），更新操作（`UpdateAction`）等。通过定义期望的 Action，在单元测试中可以检查 clientset 是否发起了与期望一致的请求。\n\n##### 利用 fixture 实现测试用例\n\n接下来，以一个 Foo Operator 的测试用例为例，介绍一下如何使用 fixture 实现单元测试用例：\n\n```go\nfunc TestCreatesDeployment(t *testing.T) {\n\tf := newFixture(t)\n\tfoo := newFoo(\"test\", int32Ptr(1))\n\n\tf.fooLister = append(f.fooLister, foo)\n\tf.objects = append(f.objects, foo)\n\n\texpDeployment := newDeployment(foo)\n\tf.expectCreateDeploymentAction(expDeployment)\n\tf.expectUpdateFooStatusAction(foo)\n\n\tf.run(getKey(foo, t))\n}\n```\n\n这一测试用例用于测试创建 Deployment 的逻辑是否符合期望。首先创建出一 fixture 对象，其次构造一个用于测试的 Foo 实例。然后将 Foo 添加到 `fooLister` 和 `objects` 中。最后，构造期望的 Deployment，利用辅助函数 `expectCreateDeploymentAction` 和 `expectUpdateFooStatusAction` 将对应的期望 Action 加入到 `kubeactions` 和 `actions` 中。最后，运行 Controller 以完成整个测试。\n\n接下来，看一下 `f.run(getKey(foo, t))` 具体的过程。\n\n\u003cdetails\u003e\n  \u003csummary\u003e点击此处查看 run 代码\u003c/summary\u003e\n\n```go\nfunc (f *fixture) run(fooName string) {\n\tf.runController(fooName, true, false)\n}\n\nfunc (f *fixture) runController(fooName string, startInformers bool, expectError bool) {\n\tc, i, k8sI := f.newController()\n\tif startInformers {\n\t\tstopCh := make(chan struct{})\n\t\tdefer close(stopCh)\n\t\ti.Start(stopCh)\n\t\tk8sI.Start(stopCh)\n\t}\n\n\terr := c.syncHandler(fooName)\n\tif !expectError \u0026\u0026 err != nil {\n\t\tf.t.Errorf(\"error syncing foo: %v\", err)\n\t} else if expectError \u0026\u0026 err == nil {\n\t\tf.t.Error(\"expected error syncing foo, got nil\")\n\t}\n\n\tactions := filterInformerActions(f.client.Actions())\n\tfor i, action := range actions {\n\t\tif len(f.actions) \u003c i+1 {\n\t\t\tf.t.Errorf(\"%d unexpected actions: %+v\", len(actions)-len(f.actions), actions[i:])\n\t\t\tbreak\n\t\t}\n\n\t\texpectedAction := f.actions[i]\n\t\tcheckAction(expectedAction, action, f.t)\n\t}\n\n\tif len(f.actions) \u003e len(actions) {\n\t\tf.t.Errorf(\"%d additional expected actions:%+v\", len(f.actions)-len(actions), f.actions[len(actions):])\n\t}\n\n\tk8sActions := filterInformerActions(f.kubeclient.Actions())\n\tfor i, action := range k8sActions {\n\t\tif len(f.kubeactions) \u003c i+1 {\n\t\t\tf.t.Errorf(\"%d unexpected actions: %+v\", len(k8sActions)-len(f.kubeactions), k8sActions[i:])\n\t\t\tbreak\n\t\t}\n\n\t\texpectedAction := f.kubeactions[i]\n\t\tcheckAction(expectedAction, action, f.t)\n\t}\n\n\tif len(f.kubeactions) \u003e len(k8sActions) {\n\t\tf.t.Errorf(\"%d additional expected actions:%+v\", len(f.kubeactions)-len(k8sActions), f.kubeactions[len(k8sActions):])\n\t}\n}\n\nfunc (f *fixture) newController() (*Controller, informers.SharedInformerFactory, kubeinformers.SharedInformerFactory) {\n\tf.client = fake.NewSimpleClientset(f.objects...)\n\tf.kubeclient = k8sfake.NewSimpleClientset(f.kubeobjects...)\n\n\ti := informers.NewSharedInformerFactory(f.client, noResyncPeriodFunc())\n\tk8sI := kubeinformers.NewSharedInformerFactory(f.kubeclient, noResyncPeriodFunc())\n\n\tc := NewController(f.kubeclient, f.client,\n\t\tk8sI.Apps().V1().Deployments(), i.Samplecontroller().V1alpha1().Foos())\n\n\tc.foosSynced = alwaysReady\n\tc.deploymentsSynced = alwaysReady\n\tc.recorder = \u0026record.FakeRecorder{}\n\n\tfor _, f := range f.fooLister {\n\t\ti.Samplecontroller().V1alpha1().Foos().Informer().GetIndexer().Add(f)\n\t}\n\n\tfor _, d := range f.deploymentLister {\n\t\tk8sI.Apps().V1().Deployments().Informer().GetIndexer().Add(d)\n\t}\n\n\treturn c, i, k8sI\n}\n```\n\u003c/details\u003e\n\nrun 是对另一函数 `runController(fooName string, startInformers bool, expectError bool)` 的直接调用。其中 `fooName` 就是 Foo 的 `namespace/name`，这一参数会被用来作为 `syncHandler` 的输入。第二个参数 `startInformers` 确定是否需要利用 goroutine 运行 informer 的逻辑。第三个参数 `expectError` 代表是否期望在运行中收到 error。\n\n在 `runController` 的最开始，通过调用 `newController`，创建了 fake 的 client 和 informer，并且将数据在 client 和 informer 中准备好。接下来，是测试用例中的主要逻辑，它会把 informer 运行起来，同时去调用一次 `syncHandler`，做一次状态的比对和同步，最后检查在 client 中，是否有期望的 Action 发生。\n\n在这一例子中，我们期望的 Action 是：\n\n```go\n    f.expectCreateDeploymentAction(expDeployment)\n\tf.expectUpdateFooStatusAction(foo)\n```\n\n也就是期望观测到创建 `expDeployment` 的 Action，以及更新 `Foo` 的状态的 Action。如果在测试用例运行时没有在 `runController` 时遇到这两个 Action，测试用例就会报错。\n\n### Lab 1 实现单元测试\n\n#### 问题\n\n目前在代码中，已经有了四个测试用例，分别是 `TestCreatesDeployment`，`TestDoNothing`，`TestUpdateDeployment` 和 `TestNotControlledByUs`。Lab 需要完成一个新的测试用例：`TestAnonymousDeployment`。\n\n在 `TestAnonymousDeployment` 中，用户需要测试 `Foo.Spec.DeploymentName` 为空的情况。在实现时，建议利用 `Fixture` 简化实现，具体细节可参考已有的三个测试用例。\n\n请前往 `$GOPATH/src/k8s.io/sample-controller/controller_test.go` 实现用例 `TestAnonymousDeployment`。\n\n#### 参考实现\n\n在完成后，可以查看参考实现。实现方式有很多种，此处只提供其中的一种实现方式。\n\n\u003cdetails\u003e\n  \u003csummary\u003e点击此处查看参考实现\u003c/summary\u003e\n\n```go\nfunc TestAnonymousDeployment(t *testing.T) {\n\tf := newFixture(t)\n\tfoo := newFoo(\"test\", int32Ptr(1))\n\tfoo.Spec.DeploymentName = \"\"\n\n\tf.fooLister = append(f.fooLister, foo)\n\tf.objects = append(f.objects, foo)\n\n\tf.run(getKey(foo, t))\n}\n```\n\n首先，利用 newFixture 创建了测试环境，然后创建了 `DeploymentName` 是空值的测试用例 Foo，然后将其加入到了 `fooLister` 和 `objects` 中，在 `run` 的调用中，`fooLister` 和 `objects` 中的对象会被加入到 operator 对应的 `client` 和 `informer` 中。最后，由于在 `DeploymentName` 是空值的情况下，会直接返回，不做任何处理：\n\n```go\n    if deploymentName == \"\" {\n\t\t// We choose to absorb the error here as the worker would requeue the\n\t\t// resource otherwise. Instead, the next time the resource is updated\n\t\t// the resource will be queued again.\n\t\tutilruntime.HandleError(fmt.Errorf(\"%s: deployment name must be specified\", key))\n\t\treturn nil\n\t}\n```\n\n所以，应该没有任何 Action 产生。\n\n\u003c/details\u003e\n\n### Lab 2 扩展内容：Table Driven Test\n\n#### 背景知识\n\n在之前的实验中，所有的测试用例都是独立的，我们为了不同的情况都实现了一个 `TestXXX` 函数，这样的实现，当我们要覆盖更多 case 时，会非常冗长。这时我们可以采用 Table-Driven 的方式，把多个测试用例合并在一个用例中。举一个斐波那契数列的例子介绍这样的方式：\n\n```go\nfunc TestFib(t *testing.T) {\n    var fibTests = []struct {\n        in       int // input\n        expected int // expected result\n    }{\n        {1, 1},\n        {2, 1},\n        {3, 2},\n        {4, 3},\n        {5, 5},\n        {6, 8},\n        {7, 13},\n    }\n\n    for _, tt := range fibTests {\n        actual := Fib(tt.in)\n        if actual != tt.expected {\n            t.Errorf(\"Fib(%d) = %d; expected %d\", tt.in, actual, tt.expected)\n        }\n    }\n}\n```\n\n通过定义了一个测试用例的数组，在一个循环中依次进行多次测试。这样的实现可以用更少的代码覆盖更多的用例，更多介绍可以参考 [golang/go/wiki/TableDrivenTests](https://github.com/golang/go/wiki/TableDrivenTests)。\n\n#### 问题\n\n在这一实验中，我们需要把之前的五个测试用例，利用 Table Driven 的方法，合并成一个测试用例。\n\n请前往 `$GOPATH/src/k8s.io/sample-controller/controller_test.go` 实现用例 `TestController`。\n\n#### 参考实现\n\n在完成后，可以查看参考实现。实现方式有很多种，此处只提供其中的一种实现方式。\n\n\u003cdetails\u003e\n  \u003csummary\u003e点击此处查看参考实现\u003c/summary\u003e\n\n首先，在测试函数中定义了一个结构 `TestCase`，其中包含了测试用例的名字，测试中会用到的数据 `Foo` 和 `Deployment`，控制是否将数据加入到 Controller 中的变量 `AddFooIntoController` 和 `AddDeploymentIntoController`。接下来是控制是否期望观测到对应 Action 的一系列变量 `ExpectCreateDeployment`，`ExpectUpdateDeployment` 和 `ExpectUpdateFooStatus`。最后是关于期望观测到的 Deployment 和是否期望遇到 Error 的变量 `ExpectDeployment` 和 `ExpectError`。\n\n```go\nfunc TestController(t *testing.T) {\n\ttype TestCase struct {\n\t\tCase       string\n\t\tFoo        *samplecontroller.Foo\n\t\tDeployment *appsv1.Deployment\n\n\t\tAddFooIntoController        bool\n\t\tAddDeploymentIntoController bool\n\n\t\tExpectCreateDeployment bool\n\t\tExpectUpdateDeployment bool\n\t\tExpectUpdateFooStatus  bool\n\n\t\tExpectDeployment *appsv1.Deployment\n\t\tExpectError      bool\n\t}\n\ttestCases := []TestCase{\n\t\t{\n\t\t\tCase:       \"TestCreatesDeployment\",\n\t\t\tFoo:        newFoo(\"test\", int32Ptr(1)),\n\t\t\tDeployment: newDeployment(newFoo(\"test\", int32Ptr(1))),\n\n\t\t\tAddFooIntoController:        true,\n\t\t\tAddDeploymentIntoController: false,\n\n\t\t\tExpectCreateDeployment: true,\n\t\t\tExpectUpdateDeployment: false,\n\t\t\tExpectUpdateFooStatus:  true,\n\n\t\t\tExpectError: false,\n\t\t},\n\t\t{\n\t\t\tCase:       \"TestDoNothing\",\n\t\t\tFoo:        newFoo(\"test\", int32Ptr(1)),\n\t\t\tDeployment: newDeployment(newFoo(\"test\", int32Ptr(1))),\n\n\t\t\tAddFooIntoController:        true,\n\t\t\tAddDeploymentIntoController: true,\n\n\t\t\tExpectCreateDeployment: false,\n\t\t\tExpectUpdateDeployment: false,\n\t\t\tExpectUpdateFooStatus:  true,\n\n\t\t\tExpectError: false,\n\t\t},\n\t\t{\n\t\t\tCase:       \"TestUpdateDeployment\",\n\t\t\tFoo:        newFoo(\"test\", int32Ptr(1)),\n\t\t\tDeployment: newDeployment(newFoo(\"test\", int32Ptr(2))),\n\n\t\t\tAddFooIntoController:        true,\n\t\t\tAddDeploymentIntoController: true,\n\n\t\t\tExpectCreateDeployment: false,\n\t\t\tExpectUpdateDeployment: true,\n\t\t\tExpectUpdateFooStatus:  true,\n\n\t\t\tExpectDeployment: newDeployment(newFoo(\"test\", int32Ptr(1))),\n\t\t\tExpectError:      false,\n\t\t},\n\t\t{\n\t\t\tCase: \"TestNotControlledByUs\",\n\t\t\tFoo:  newFoo(\"test\", int32Ptr(1)),\n\t\t\tDeployment: func() *appsv1.Deployment {\n\t\t\t\td := newDeployment(newFoo(\"test\", int32Ptr(2)))\n\t\t\t\td.ObjectMeta.OwnerReferences = []metav1.OwnerReference{}\n\t\t\t\treturn d\n\t\t\t}(),\n\n\t\t\tAddFooIntoController:        true,\n\t\t\tAddDeploymentIntoController: true,\n\n\t\t\tExpectCreateDeployment: false,\n\t\t\tExpectUpdateDeployment: false,\n\t\t\tExpectUpdateFooStatus:  false,\n\n\t\t\tExpectError: true,\n\t\t},\n\t\t{\n\t\t\tCase: \"TestAnonymousDeployment\",\n\t\t\tFoo: func() *samplecontroller.Foo {\n\t\t\t\tf := newFoo(\"test\", int32Ptr(1))\n\t\t\t\tf.Spec.DeploymentName = \"\"\n\t\t\t\treturn f\n\t\t\t}(),\n\n\t\t\tAddFooIntoController:        true,\n\t\t\tAddDeploymentIntoController: false,\n\n\t\t\tExpectCreateDeployment: false,\n\t\t\tExpectUpdateDeployment: false,\n\t\t\tExpectUpdateFooStatus:  false,\n\n\t\t\tExpectError: false,\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tt.Logf(\"Running Test Case: %s\", testCase.Case)\n\t\tf := newFixture(t)\n\t\tif testCase.AddFooIntoController {\n\t\t\tf.fooLister = append(f.fooLister, testCase.Foo)\n\t\t\tf.objects = append(f.objects, testCase.Foo)\n\t\t}\n\t\tif testCase.AddDeploymentIntoController {\n\t\t\tf.deploymentLister = append(f.deploymentLister, testCase.Deployment)\n\t\t\tf.kubeobjects = append(f.kubeobjects, testCase.Deployment)\n\t\t}\n\t\tif testCase.ExpectCreateDeployment {\n\t\t\tf.expectCreateDeploymentAction(testCase.Deployment)\n\t\t}\n\t\tif testCase.ExpectUpdateDeployment {\n\t\t\tif testCase.ExpectDeployment != nil {\n\t\t\t\tf.expectUpdateDeploymentAction(testCase.ExpectDeployment)\n\t\t\t} else {\n\t\t\t\tf.expectUpdateDeploymentAction(testCase.Deployment)\n\t\t\t}\n\t\t}\n\t\tif testCase.ExpectUpdateFooStatus {\n\t\t\tf.expectUpdateFooStatusAction(testCase.Foo)\n\t\t}\n\t\tf.runController(getKey(testCase.Foo, t), true, testCase.ExpectError)\n\t}\n}\n```\n\n接下来，就顺理成章了。添加测试用例只需要在 `testCases` 中添加新的 `TestCase` 实例即可。\n\n\u003c/details\u003e\n\n## 为 kubebuilder v1 生成的 Operator 实现单元测试（TODO）\n\n## 为 kubebuilder v2 生成的 Operator 实现单元测试（TODO）\n\n\u003c!-- ### 事先需要了解的知识\n\nTODO\n\n### 准备工作\n\n首先，将 `kubebuilder-v2-demo-operator` 复制到 `$GOPATH/src/github.com/caicloud/kbv2-operator`。\n\n```sh\n# 将 `kubebuilder-v2-demo-operator` 复制到 `$GOPATH/src/github.com/caicloud/kbv2-operator`。\n./scripts/install-kubebuilder-v2-operator.sh\n# 到 `$GOPATH/src/github.com/caicloud/kbv2-operator` 目录下\ncd $GOPATH/src/github.com/caicloud/kbv2-operator\n```\n\n这一操作是为了确保 operator 在正确的路径下。此时已经准备好了 Operator 的环境。\n\n`kubebuilder-v2-demo-operator` 目录下的代码，是由 [kubebuilder v2.0.0-beta.0](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/v2.0.0-beta.0) 生成的代码，生成命令为：\n\n注：由于代码已经生成好，所以不需要再执行上面的命令，此处的记录只是为了保证可复现。\n\n```sh\nGO111MODULE=\"on\" kubebuilder init --domain caicloud.io --license apache2 --owner \"The Operator authors\"\nGO111MODULE=\"on\" kubebuilder create api --group demo --version v1 --kind Demo\n```\n\n### 基于 kubebuilder 的单元测试\n\nTODO --\u003e","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcaicloud%2Flearning-unit-testing-for-k8s-operator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcaicloud%2Flearning-unit-testing-for-k8s-operator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcaicloud%2Flearning-unit-testing-for-k8s-operator/lists"}