-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add envtest testing docs to extend cronjob example
- Loading branch information
Showing
6 changed files
with
443 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
243 changes: 243 additions & 0 deletions
243
docs/book/src/cronjob-tutorial/testdata/project/controllers/cronjob_controller_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
/* | ||
We can create the tests in a separate file, cronjob_controller_test.go. | ||
Keeping consistent with the Gingko usage in our autogenerated suite_test.go code, we’ll use Gingko to write our tests. | ||
*/ | ||
|
||
/* | ||
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 necessary imports. | ||
*/ | ||
package controllers | ||
|
||
import ( | ||
"context" | ||
"reflect" | ||
"time" | ||
|
||
cronjobv1 "tutorial.kubebuilder.io/project/api/v1" | ||
|
||
. "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/runtime/schema" | ||
"k8s.io/apimachinery/pkg/types" | ||
) | ||
|
||
/* | ||
We then set up consts we'll use in our tests, including test names for our objects as well as helper definitions | ||
like SUCCESS and FAILURE to make our tests more readable. The usage of timeout and interval durations will become evident soon. | ||
*/ | ||
const ( | ||
CronjobName string = "test-cronjob" | ||
CronjobNamespace string = "test-cronjob-namespace" | ||
JobName string = "test-job" | ||
|
||
SUCCESS bool = true | ||
FAILURE bool = false | ||
|
||
timeout = time.Second * 10 | ||
interval = time.Millisecond * 250 | ||
) | ||
|
||
/* | ||
We also expose the GVK for our CronJob version, which we will use to manage ownership of Jobs that belong to our test CronJob. | ||
One way you can set this GVK is by defining it as a global var like so: | ||
*/ | ||
var ( | ||
Kind string = reflect.TypeOf(cronjobv1.CronJob{}).Name() | ||
GVK schema.GroupVersionKind = cronjobv1.GroupVersion.WithKind(Kind) | ||
) | ||
|
||
/* | ||
We still have a little more setup before we can write our tests. | ||
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. | ||
We define a `testHelper_GetCronJobStub()` function, as well as stub creation functions for downstream objects like `testHelper_GetJobTemplateSpecStub()`, | ||
that can create a CronJob definition for us. | ||
Some k8s Kinds require values for certain spec fields, so you’ll need to make sure your stubs populate those required fields. | ||
*/ | ||
func testHelper_GetCronJobStub(name string, namespace string) *cronjobv1.CronJob { | ||
return &cronjobv1.CronJob{ | ||
TypeMeta: metav1.TypeMeta{ | ||
APIVersion: "batch.tutorial.kubebuilder.io/v1", | ||
Kind: "CronJob", | ||
}, | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: name, | ||
Namespace: namespace, | ||
}, | ||
Spec: cronjobv1.CronJobSpec{ | ||
// For simplicity, we only fill out the required fields and fall back to the CronJob CRD's defaults wherever possible. | ||
// Runs every minute! | ||
Schedule: "1 * * * *", | ||
JobTemplate: *testHelper_GetJobTemplateSpecStub(), | ||
}, | ||
} | ||
} | ||
|
||
/* | ||
We also create a stubbed Job that will help us create Jobs that belong to our test CronJob. | ||
We again make use of a stub function to create our Job, and make use of downstream stub functions like `testHelper_GetJobTemplateSpecStub()`. | ||
*/ | ||
|
||
func testHelper_JobStub(name string, namespace string, jobStatus batchv1.JobStatus) *batchv1.Job { | ||
return &batchv1.Job{ | ||
ObjectMeta: metav1.ObjectMeta{ | ||
Name: name, | ||
Namespace: namespace, | ||
}, | ||
Spec: batchv1.JobSpec{ | ||
Template: *testHelper_PodTemplateSpecStub(), | ||
}, | ||
Status: jobStatus, | ||
} | ||
} | ||
|
||
/* | ||
Given that CronJob and Job are a parents of JobTemplate objects, you’ll end up having to create | ||
some stubs for downstream objects that belong to your CronJob. | ||
*/ | ||
|
||
func testHelper_GetJobTemplateSpecStub() *batchv1beta1.JobTemplateSpec { | ||
return &batchv1beta1.JobTemplateSpec{ | ||
Spec: batchv1.JobSpec{ | ||
// For simplicity, we only fill out the required fields. | ||
Template: *testHelper_PodTemplateSpecStub(), | ||
}, | ||
} | ||
} | ||
|
||
func testHelper_PodTemplateSpecStub() *v1.PodTemplateSpec { | ||
return &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, | ||
}, | ||
} | ||
} | ||
|
||
/* | ||
Now we set up the Gingko testing framework to run CronJob controller tests. | ||
*/ | ||
var _ = Describe("Run CronJob Controller", func() { | ||
/* | ||
In this test, we use our test k8sClient to create an instance of CronJob! | ||
*/ | ||
Context("Can create cronjob", func() { | ||
It("Should create all sub resources successfully", func() { | ||
By("Create cronjob") | ||
ctx := context.Background() | ||
// Creates CronJob test-cronjob in our test cluster. | ||
cronjobCRD := testHelper_GetCronJobStub(CronjobName, CronjobNamespace) | ||
Expect(k8sClient.Create(ctx, cronjobCRD)).Should(Succeed()) | ||
}) | ||
}) | ||
|
||
/* | ||
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. | ||
*/ | ||
|
||
Context("Can perform status updates", func() { | ||
It("CronJob Status.Active count should increase as Jobs are added", func() { | ||
ctx := context.Background() | ||
|
||
/* | ||
First, we should get the test CronJob we created earlier, and verify that it currently does not have any active jobs. | ||
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 * timeout period exceed the provided interval value. | ||
In the examples below, let `FAILURE bool = false` and `SUCCESS bool = true`. | ||
timeout and interval are Go Duration values of your choosing. | ||
*/ | ||
By("Cronjob status should initially have no active jobs") | ||
cronjobLookupKey := types.NamespacedName{Name: CronjobName, Namespace: CronjobNamespace} | ||
createdCronjob := &cronjobv1.CronJob{} | ||
|
||
// We'll need to retry getting this newly creatd CronJob, given that creation may not immediately happen. | ||
Eventually(func() bool { | ||
err := k8sClient.Get(ctx, cronjobLookupKey, createdCronjob) | ||
if err != nil { | ||
return FAILURE | ||
} | ||
return SUCCESS | ||
}, timeout, interval).Should((Equal(SUCCESS))) | ||
Expect(len(createdCronjob.Status.Active)).Should(Equal(0)) | ||
|
||
/* | ||
Next, we actually create a stubbed Job that will belong to our CronJob. | ||
We again make use of a stub function to create our Job, which we defined earlier with our other stub functions. | ||
We 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("Create active job underneath cronjob") | ||
testJob := testHelper_JobStub(JobName, CronjobNamespace, batchv1.JobStatus{ | ||
Active: 2, | ||
}) | ||
|
||
// Set owner reference. Note that your CronJob’s GroupVersionKind is required to set up this owner reference. | ||
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("Active Jobs should become one") | ||
Eventually(func() bool { | ||
err := k8sClient.Get(ctx, cronjobLookupKey, createdCronjob) | ||
if err != nil { | ||
return FAILURE | ||
} | ||
|
||
if len(createdCronjob.Status.Active) != 1 { | ||
return FAILURE | ||
} | ||
|
||
for _, job := range createdCronjob.Status.Active { | ||
if job.Name == JobName { | ||
return SUCCESS | ||
} | ||
} | ||
return FAILURE | ||
}, timeout, interval).Should((Equal(SUCCESS))) | ||
}) | ||
}) | ||
|
||
}) | ||
|
||
/* | ||
After writing all this code, you can run `go test ./...` in your `controllers/` directory again to run your new test! | ||
*/ |
Oops, something went wrong.