Skip to content

Commit

Permalink
Add envtest testing docs to extend cronjob example
Browse files Browse the repository at this point in the history
  • Loading branch information
gabbifish committed May 19, 2020
1 parent 1eccd8f commit 24965e1
Show file tree
Hide file tree
Showing 6 changed files with 443 additions and 12 deletions.
7 changes: 4 additions & 3 deletions docs/book/src/cronjob-tutorial/epilogue.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ 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.

If you want to learn how to write integration tests for the CronJob kind
built in this tutorial, take a look at the [Writing Tests](/reference/writing-tests.md)
documentation.

If you want more, head over to the [Multi-Version
Tutorial](/multiversion-tutorial/tutorial.md) to learn how to add new API
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
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!
*/
Loading

0 comments on commit 24965e1

Please sign in to comment.