diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 6083ebb346e..9f12ad43400 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -26,6 +26,8 @@ - [Deploying the cert manager](./cronjob-tutorial/cert-manager.md) - [Deploying webhooks](./cronjob-tutorial/running-webhook.md) + + - [Writing tests](./cronjob-tutorial/writing-tests.md) - [Epilogue](./cronjob-tutorial/epilogue.md) @@ -72,7 +74,7 @@ - [Artifacts](./reference/artifacts.md) - [Writing controller tests](./reference/writing-tests.md) - - [Using envtest in integration tests](./reference/testing/envtest.md) + - [Using envtest in integration tests](./reference/envtest.md) - [Metrics](./reference/metrics.md) diff --git a/docs/book/src/cronjob-tutorial/epilogue.md b/docs/book/src/cronjob-tutorial/epilogue.md index 2af909dead3..0e9be7c0264 100644 --- a/docs/book/src/cronjob-tutorial/epilogue.md +++ b/docs/book/src/cronjob-tutorial/epilogue.md @@ -1,8 +1,8 @@ # Epilogue By this point, we've got a pretty full-featured implementation of the -CronJob controller, and have made use of most of the features of -KubeBuilder. +CronJob controller, made use of most of the features of +KubeBuilder, and written tests for the controller using envtest. If you want more, head over to the [Multi-Version Tutorial](/multiversion-tutorial/tutorial.md) to learn how to add new API @@ -11,9 +11,6 @@ versions to a project. Additionally, you can try the following steps on your own -- we'll have a tutorial section on them Soon™: -- writing unit/integration tests (check out [envtest][envtest]) - adding [additional printer columns][printer-columns] `kubectl get` -[envtest]: https://godoc.org/sigs.k8s.io/controller-runtime/pkg/envtest - [printer-columns]: /reference/generating-crd.md#additional-printer-columns diff --git a/docs/book/src/cronjob-tutorial/testdata/project/controllers/cronjob_controller_test.go b/docs/book/src/cronjob-tutorial/testdata/project/controllers/cronjob_controller_test.go new file mode 100644 index 00000000000..f493b8a1e42 --- /dev/null +++ b/docs/book/src/cronjob-tutorial/testdata/project/controllers/cronjob_controller_test.go @@ -0,0 +1,206 @@ +/* +Ideally, we should have one `_conroller_test.go` for each controller scaffolded and called in the `test_suite.go`. +So, let's write our example test for the CronJob controller (`cronjob_controller_test.go.`) +*/ + +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// +kubebuilder:docs-gen:collapse=Apache License + +/* +As usual, we start with the necessary imports. We also define some utility variables. +*/ +package controllers + +import ( + "context" + "reflect" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + batchv1 "k8s.io/api/batch/v1" + batchv1beta1 "k8s.io/api/batch/v1beta1" + 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" +) + +// +kubebuilder:docs-gen:collapse=Imports + +/* +The first step to writing a simple integration test is to actually create an instance of CronJob you can run tests against. +Note that to create a CronJob, you’ll need to create a stub CronJob struct that contains your CronJob’s specifications. + +Note that when we create a stub CronJob, the CronJob also needs stubs of its required downstream objects. +Without the stubbed Job template spec and the Pod template spec below, the Kubernetes API will not be able to +create the CronJob. +*/ +var _ = Describe("CronJob controller", func() { + + // Define utility constants for object names and testing timeouts/durations and intervals. + const ( + CronjobName = "test-cronjob" + CronjobNamespace = "test-cronjob-namespace" + JobName = "test-job" + + timeout = time.Second * 10 + duration = time.Second * 10 + interval = time.Millisecond * 250 + ) + + Context("When updating CronJob Status", func() { + It("Should increase CronJob Status.Active count when new Jobs are created", func() { + By("By creating a new 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: batchv1beta1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + // For simplicity, we only fill out the required fields. + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + // For simplicity, we only fill out the required fields. + Containers: []v1.Container{ + { + Name: "test-container", + Image: "test-image", + }, + }, + RestartPolicy: v1.RestartPolicyOnFailure, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, cronJob)).Should(Succeed()) + + /* + After creating this CronJob, let's check that the CronJob's Spec fields match what we passed in. + Note that, because the k8s apiserver may not have finished creating a CronJob after our `Create()` call from earlier, we will use Gomega’s Eventually() testing function instead of Expect() to give the apiserver an opportunity to finish creating our CronJob. + + `Eventually()` will repeatedly run the function provided as an argument every interval seconds until + (a) the function’s output matches what’s expected in the subsequent `Should()` call, or + (b) the number of attempts * interval period exceed the provided timeout value. + + In the examples below, timeout and interval are Go Duration values of our choosing. + */ + + cronjobLookupKey := types.NamespacedName{Name: CronjobName, Namespace: CronjobNamespace} + createdCronjob := &cronjobv1.CronJob{} + + // We'll need to retry getting this newly created CronJob, given that creation may not immediately happen. + Eventually(func() bool { + err := k8sClient.Get(ctx, cronjobLookupKey, createdCronjob) + if err != nil { + return false + } + return true + }, timeout, interval).Should(BeTrue()) + // Let's make sure our Schedule string value was properly converted/handled. + Expect(createdCronjob.Spec.Schedule).Should(Equal("1 * * * *")) + /* + Now that we've created a CronJob in our test cluster, the next step is to write a test that actually tests our CronJob controller’s behavior. + Let’s test the CronJob controller’s logic responsible for updating CronJob.Status.Active with actively running jobs. + We’ll verify that when a CronJob has a single active downstream Job, its CronJob.Status.Active field contains a reference to this Job. + + First, we should get the test CronJob we created earlier, and verify that it currently does not have any active jobs. + We use Gomega's `Consistently()` check here to ensure that the active job count remains 0 over a duration of time. + */ + By("By checking the CronJob has zero active 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)) + /* + Next, we actually create a stubbed Job that will belong to our CronJob, as well as its downstream template specs. + We set the Job's status's "Active" count to 2 to simulate the Job running two pods, which means the Job is actively running. + + We then take the stubbed Job and set its owner reference to point to our test CronJob. + This ensures that the test Job belongs to, and is tracked by, our test CronJob. + Once that’s done, we create our new Job instance. + */ + By("By creating a new Job") + testJob := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: JobName, + Namespace: CronjobNamespace, + }, + Spec: batchv1.JobSpec{ + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + // For simplicity, we only fill out the required fields. + Containers: []v1.Container{ + { + Name: "test-container", + Image: "test-image", + }, + }, + RestartPolicy: v1.RestartPolicyOnFailure, + }, + }, + }, + Status: batchv1.JobStatus{ + Active: 2, + }, + } + + // Note that your CronJob’s GroupVersionKind is required to set up this owner reference. + 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()) + /* + Adding this Job to our test CronJob should trigger our controller’s reconciler logic. + After that, we can write a test that evaluates whether our controller eventually updates our CronJob’s Status field as expected! + */ + By("By checking that the CronJob has one active 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), "should list our active job %s in the active jobs list in status", JobName) + }) + }) + +}) + +/* + After writing all this code, you can run `go test ./...` in your `controllers/` directory again to run your new test! +*/ diff --git a/docs/book/src/cronjob-tutorial/testdata/project/controllers/suite_test.go b/docs/book/src/cronjob-tutorial/testdata/project/controllers/suite_test.go new file mode 100644 index 00000000000..ea3d0d151b0 --- /dev/null +++ b/docs/book/src/cronjob-tutorial/testdata/project/controllers/suite_test.go @@ -0,0 +1,154 @@ +/* +When we created the CronJob API with `kubebuilder create api` in a [previous chapter](/cronjob-tutorial/new-api.md), Kubebuilder already did some test work for you. +Kubebuilder scaffolded a `controllers/suite_test.go` file that does the bare bones of setting up a test environment. + +First, it will contain the necessary imports. +*/ + +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// +kubebuilder:docs-gen:collapse=Apache License + +package controllers + +import ( + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + batchv1 "tutorial.kubebuilder.io/project/api/v1" + // +kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +// +kubebuilder:docs-gen:collapse=Imports + +/* +Now, let's go through the code generated. +*/ + +var cfg *rest.Config +var k8sClient client.Client // You'll be using this client in your tests. +var testEnv *envtest.Environment + +var _ = BeforeSuite(func(done Done) { + logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) + + /* + First, the envtest cluster is configured to read CRDs from the CRD directory Kubebuilder scaffolds for you. + */ + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + } + + /* + Then, we start the envtest cluster. + */ + var err error + cfg, err = testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + /* + The autogenerated test code will add the CronJob Kind schema to the default client-go k8s scheme. + This ensures that the CronJob API/Kind will be used in our test controller. + */ + err = batchv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + /* + After the schemas, you will see the following marker. + This marker is what allows new schemas to be added here automatically when a new API is added to the project. + */ + + // +kubebuilder:scaffold:scheme + + /* + A client is created for our test CRUD operations. + */ + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient).ToNot(BeNil()) + + /* + One thing that this autogenerated file is missing, however, is a way to actually start your controller. + The code above will set up a client for interacting with your custom Kind, + but will not be able to test your controller behavior. + If you want to test your custom controller logic, you’ll need to add some familiar-looking manager logic + to your BeforeSuite() function, so you can register your custom controller to run on this test cluster. + + You may notice that the code below runs your controller with nearly identical logic to your CronJob project’s main.go! + The only difference is that the manager is started in a separate goroutine so it does not block the cleanup of envtest + when you’re done running your tests. + + Once you've added the code below, you can actually delete the k8sClient above, because you can get k8sClient from the manager + (as shown below). + */ + + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + err = (&CronJobReconciler{ + Client: k8sManager.GetClient(), + Log: ctrl.Log.WithName("controllers").WithName("CronJob"), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + go func() { + err = k8sManager.Start(ctrl.SetupSignalHandler()) + Expect(err).ToNot(HaveOccurred()) + }() + + k8sClient = k8sManager.GetClient() + Expect(k8sClient).ToNot(BeNil()) + + close(done) +}, 60) + +/* +Kubebuilder also generates boilerplate functions for cleaning up envtest and actually running your test files in your controllers/ directory. +You won't need to touch these. +*/ + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecsWithDefaultAndCustomReporters(t, + "Controller Suite", + []Reporter{envtest.NewlineReporter{}}) +} + +/* +Now that you have your controller running on a test cluster and a client ready to perform operations on your CronJob, we can start writing integration tests! +*/ diff --git a/docs/book/src/cronjob-tutorial/testdata/project/go.mod b/docs/book/src/cronjob-tutorial/testdata/project/go.mod index 658891eba2d..828280b74a4 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/go.mod +++ b/docs/book/src/cronjob-tutorial/testdata/project/go.mod @@ -4,6 +4,8 @@ go 1.13 require ( github.com/go-logr/logr v0.1.0 + github.com/onsi/ginkgo v1.11.0 + github.com/onsi/gomega v1.8.1 github.com/robfig/cron v1.2.0 k8s.io/api v0.17.2 k8s.io/apimachinery v0.17.2 diff --git a/docs/book/src/cronjob-tutorial/testdata/project/go.sum b/docs/book/src/cronjob-tutorial/testdata/project/go.sum index 8f29df43aee..04c9a080c6e 100644 --- a/docs/book/src/cronjob-tutorial/testdata/project/go.sum +++ b/docs/book/src/cronjob-tutorial/testdata/project/go.sum @@ -124,10 +124,14 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7 h1:u4bArs140e9+AfE52mFHOXVFnOSBJBRlzTHrOPLOIhE= github.com/golang/groupcache v0.0.0-20180513044358-24b0969c4cb7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -144,6 +148,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA= @@ -248,6 +254,9 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -299,6 +308,8 @@ go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qL go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= @@ -314,6 +325,8 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -370,6 +383,8 @@ golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456 h1:ng0gs1AKnRRuEMZoTLLlbOd+C17zUDepwGQBb/n+JVg= golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7 h1:HmbHVPwrPEKPGLAcHSrMe6+hqSUlvZU0rab6x5EXfGU= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -435,19 +450,29 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= k8s.io/api v0.17.2 h1:NF1UFXcKN7/OOv1uxdRz3qfra8AHsPav5M93hlV9+Dc= k8s.io/api v0.17.2/go.mod h1:BS9fjjLc4CMuqfSO8vgbHPKMt5+SF0ET6u/RVDihTo4= +k8s.io/api v0.18.2 h1:wG5g5ZmSVgm5B+eHMIbI9EGATS2L8Z72rda19RIEgY8= +k8s.io/api v0.18.2/go.mod h1:SJCWI7OLzhZSvbY7U8zwNl9UA4o1fizoug34OV/2r78= k8s.io/apiextensions-apiserver v0.17.2 h1:cP579D2hSZNuO/rZj9XFRzwJNYb41DbNANJb6Kolpss= k8s.io/apiextensions-apiserver v0.17.2/go.mod h1:4KdMpjkEjjDI2pPfBA15OscyNldHWdBCfsWMDWAmSTs= +k8s.io/apiextensions-apiserver v0.18.2 h1:I4v3/jAuQC+89L3Z7dDgAiN4EOjN6sbm6iBqQwHTah8= +k8s.io/apiextensions-apiserver v0.18.2/go.mod h1:q3faSnRGmYimiocj6cHQ1I3WpLqmDgJFlKL37fC4ZvY= k8s.io/apimachinery v0.17.2 h1:hwDQQFbdRlpnnsR64Asdi55GyCaIP/3WQpMmbNBeWr4= k8s.io/apimachinery v0.17.2/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/apimachinery v0.18.2 h1:44CmtbmkzVDAhCpRVSiP2R5PPrC2RtlIv/MoB8xpdRA= +k8s.io/apimachinery v0.18.2/go.mod h1:9SnR/e11v5IbyPCGbvJViimtJ0SwHG4nfZFjU77ftcA= k8s.io/apiserver v0.17.2/go.mod h1:lBmw/TtQdtxvrTk0e2cgtOxHizXI+d0mmGQURIHQZlo= k8s.io/client-go v0.17.2 h1:ndIfkfXEGrNhLIgkr0+qhRguSD3u6DCmonepn1O6NYc= k8s.io/client-go v0.17.2/go.mod h1:QAzRgsa0C2xl4/eVpeVAZMvikCn8Nm81yqVx3Kk9XYI= +k8s.io/client-go v0.18.2 h1:aLB0iaD4nmwh7arT2wIn+lMnAq7OswjaejkQ8p9bBYE= +k8s.io/client-go v0.18.2/go.mod h1:Xcm5wVGXX9HAA2JJ2sSBUn3tCJ+4SVlCbl2MNNv+CIU= k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= k8s.io/component-base v0.17.2/go.mod h1:zMPW3g5aH7cHJpKYQ/ZsGMcgbsA/VyhEugF3QT1awLs= k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= @@ -459,8 +484,12 @@ k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a h1:UcxjrRMyNx/i/y8G7kPvLyy7rfbeuf1PYyBf973pgyU= k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c h1:/KUFqjjqAcY4Us6luF5RDNZ16KJtb49HfR3ZHB9qYXM= +k8s.io/kube-openapi v0.0.0-20200121204235-bf4fb3bd569c/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f h1:GiPwtSzdP43eI1hpPCbROQCCIgCuiMMNF8YUVLF3vJo= k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89 h1:d4vVOjXm687F1iLSP2q3lyPPuyvTUt3aVoBpi2DqRsU= +k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= @@ -468,7 +497,14 @@ modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= sigs.k8s.io/controller-runtime v0.5.0 h1:CbqIy5fbUX+4E9bpnBFd204YAzRYlM9SWW77BbrcDQo= sigs.k8s.io/controller-runtime v0.5.0/go.mod h1:REiJzC7Y00U+2YkMbT8wxgrsX5USpXKGhb2sCtAXiT8= +sigs.k8s.io/controller-runtime v0.6.0 h1:Fzna3DY7c4BIP6KwfSlrfnj20DJ+SeMBK8HSFvOk9NM= +sigs.k8s.io/controller-runtime v0.6.0/go.mod h1:CpYf5pdNY/B352A1TFLAS2JVSlnGQ5O2cftPHndTroo= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E= +sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/docs/book/src/cronjob-tutorial/writing-tests.md b/docs/book/src/cronjob-tutorial/writing-tests.md new file mode 100644 index 00000000000..bea255c5e13 --- /dev/null +++ b/docs/book/src/cronjob-tutorial/writing-tests.md @@ -0,0 +1,33 @@ +# Writing controller tests + +Testing Kubernetes controllers is a big subject, and the boilerplate testing +files generated for you by kubebuilder are fairly minimal. + +To walk you through integration testing patterns for Kubebuilder-generated controllers, we will revisit the CronJob we built in our first tutorial and write a simple test for it. + +The basic approach is that, in your generated `suite_test.go` file, you will use envtest to create a local Kubernetes API server, instantiate and run your controllers, and then write additional `*_test.go` files to test it using [Ginko](http://onsi.github.io/ginkgo). + +If you want to tinker with how your envtest cluster is configured, see section [Writing and Running Integration Tests](/reference/testing/envtest.md) as well as the [`envtest docs`](https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/envtest). + +## Test Environment Setup + +{{#literatego ../cronjob-tutorial/testdata/project/controllers/suite_test.go}} + +## Testing your Controller's Behavior + +{{#literatego ../cronjob-tutorial/testdata/project/controllers/cronjob_controller_test.go}} + +This Status update example above demonstrates a general testing strategy for a custom Kind with downstream objects. By this point, you hopefully have learned the following methods for testing your controller behavior: + +* Setting up your controller to run on an envtest cluster +* Writing stubs for creating test objects +* Isolating changes to an object to test specific controller behavior + +## Advanced Examples + +There are more involved examples of using envtest to rigorously test controller behavior. Examples include: + +* Azure Databricks Operator: see their fully fleshed-out + [`suite_test.go`](https://github.com/microsoft/azure-databricks-operator/blob/0f722a710fea06b86ecdccd9455336ca712bf775/controllers/suite_test.go) + as well as any `*_test.go` file in that directory [like this + one](https://github.com/microsoft/azure-databricks-operator/blob/0f722a710fea06b86ecdccd9455336ca712bf775/controllers/secretscope_controller_test.go). diff --git a/docs/book/src/reference/testing/envtest.md b/docs/book/src/reference/envtest.md similarity index 100% rename from docs/book/src/reference/testing/envtest.md rename to docs/book/src/reference/envtest.md diff --git a/docs/book/src/reference/writing-tests.md b/docs/book/src/reference/writing-tests.md deleted file mode 100644 index 96eb69e55a2..00000000000 --- a/docs/book/src/reference/writing-tests.md +++ /dev/null @@ -1,19 +0,0 @@ -# Writing controller tests - -Testing Kubernetes controller is a big subject, and the boilerplate testing -files generated for you by kubebuilder are fairly minimal. - -[Writing and Running Integration Tests](/reference/testing/envtest.md) documents steps to consider when writing integration steps for your controllers, and available options for configuring your test control plane using [`envtest`](https://godoc.org/github.com/kubernetes-sigs/controller-runtime/pkg/envtest). - -Until more documentation has been written, your best bet to get started is to look at some -existing examples, such as: - -* Azure Databricks Operator: see their fully fleshed-out - [`suite_test.go`](https://github.com/microsoft/azure-databricks-operator/blob/0f722a710fea06b86ecdccd9455336ca712bf775/controllers/suite_test.go) - as well as any `*_test.go` file in that directory [like this - one](https://github.com/microsoft/azure-databricks-operator/blob/0f722a710fea06b86ecdccd9455336ca712bf775/controllers/secretscope_controller_test.go). - -The basic approach is that, in your generated `suite_test.go` file, you will -create a local Kubernetes API server, instantiate and run your controllers, and -then write additional `*_test.go` files to test it using -[Ginko](http://onsi.github.io/ginkgo).