Skip to content

Commit 9b054aa

Browse files
schrejJoelSpeed
andcommitted
Add komega package with helpers for writing tests
The helpers can be used to write tests using the gomega library in conjunction with the Kubernetes API, or a mock of it. Co-Authored-By: Joel Speed <[email protected]>
1 parent eb292e5 commit 9b054aa

File tree

8 files changed

+558
-5
lines changed

8 files changed

+558
-5
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ require (
88
github.com/go-logr/logr v1.2.0
99
github.com/go-logr/zapr v1.2.0
1010
github.com/onsi/ginkgo v1.16.5
11-
github.com/onsi/gomega v1.17.0
11+
github.com/onsi/gomega v1.18.1
1212
github.com/prometheus/client_golang v1.11.1
1313
github.com/prometheus/client_model v0.2.0
1414
go.uber.org/goleak v1.1.12
1515
go.uber.org/zap v1.19.1
16-
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8
16+
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
1717
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
1818
gomodules.xyz/jsonpatch/v2 v2.2.0
1919
k8s.io/api v0.23.0

go.sum

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
233233
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
234234
github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
235235
github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
236+
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
236237
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
237238
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
238239
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -353,11 +354,14 @@ github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9k
353354
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
354355
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
355356
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
357+
github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ=
358+
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
356359
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
357360
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
358361
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
359-
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
360362
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
363+
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
364+
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
361365
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
362366
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
363367
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
@@ -676,8 +680,8 @@ golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBc
676680
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
677681
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
678682
golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
679-
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8 h1:M69LAlWZCshgp0QSzyDcSsSIejIEeuaCVpmwcKwyLMk=
680-
golang.org/x/sys v0.0.0-20211029165221-6e7872819dc8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
683+
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
684+
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
681685
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
682686
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE=
683687
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

pkg/envtest/komega/OWNERS

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
approvers:
2+
- controller-runtime-admins
3+
- controller-runtime-maintainers
4+
- controller-runtime-approvers
5+
- schrej
6+
- JoelSpeed
7+
- sbueringer
8+
reviewers:
9+
- controller-runtime-admins
10+
- controller-runtime-reviewers
11+
- controller-runtime-approvers
12+
- schrej
13+
- JoelSpeed
14+
- sbueringer

pkg/envtest/komega/default.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package komega
2+
3+
import (
4+
"context"
5+
6+
"sigs.k8s.io/controller-runtime/pkg/client"
7+
)
8+
9+
// defaultK is the Komega used by the package global functions.
10+
var defaultK = &komega{ctx: context.Background()}
11+
12+
// SetClient sets the client used by the package global functions.
13+
func SetClient(c client.Client) {
14+
defaultK.client = c
15+
}
16+
17+
func checkDefaultClient() {
18+
if defaultK.client == nil {
19+
panic("Default Komega's client is not set. Use SetClient to set it.")
20+
}
21+
}
22+
23+
// Get returns a function that fetches a resource and returns the occurring error.
24+
// It can be used with gomega.Eventually() like this
25+
// deployment := appsv1.Deployment{ ... }
26+
// gomega.Eventually(komega.Get(&deployment)).To(gomega.Succeed())
27+
// By calling the returned function directly it can also be used with gomega.Expect(komega.Get(...)()).To(...)
28+
func Get(obj client.Object) func() error {
29+
checkDefaultClient()
30+
return defaultK.Get(obj)
31+
}
32+
33+
// List returns a function that lists resources and returns the occurring error.
34+
// It can be used with gomega.Eventually() like this
35+
// deployments := v1.DeploymentList{ ... }
36+
// gomega.Eventually(k.List(&deployments)).To(gomega.Succeed())
37+
// By calling the returned function directly it can also be used as gomega.Expect(k.List(...)()).To(...)
38+
func List(list client.ObjectList, opts ...client.ListOption) func() error {
39+
checkDefaultClient()
40+
return defaultK.List(list, opts...)
41+
}
42+
43+
// Update returns a function that fetches a resource, applies the provided update function and then updates the resource.
44+
// It can be used with gomega.Eventually() like this:
45+
// deployment := appsv1.Deployment{ ... }
46+
// gomega.Eventually(k.Update(&deployment, func (o client.Object) {
47+
// deployment.Spec.Replicas = 3
48+
// return &deployment
49+
// })).To(gomega.Scucceed())
50+
// By calling the returned function directly it can also be used as gomega.Expect(k.Update(...)()).To(...)
51+
func Update(obj client.Object, f func(), opts ...client.UpdateOption) func() error {
52+
checkDefaultClient()
53+
return defaultK.Update(obj, f, opts...)
54+
}
55+
56+
// UpdateStatus returns a function that fetches a resource, applies the provided update function and then updates the resource's status.
57+
// It can be used with gomega.Eventually() like this:
58+
// deployment := appsv1.Deployment{ ... }
59+
// gomega.Eventually(k.Update(&deployment, func (o client.Object) {
60+
// deployment.Status.AvailableReplicas = 1
61+
// return &deployment
62+
// })).To(gomega.Scucceed())
63+
// By calling the returned function directly it can also be used as gomega.Expect(k.UpdateStatus(...)()).To(...)
64+
func UpdateStatus(obj client.Object, f func(), opts ...client.UpdateOption) func() error {
65+
checkDefaultClient()
66+
return defaultK.UpdateStatus(obj, f, opts...)
67+
}
68+
69+
// Object returns a function that fetches a resource and returns the object.
70+
// It can be used with gomega.Eventually() like this:
71+
// deployment := appsv1.Deployment{ ... }
72+
// gomega.Eventually(k.Object(&deployment)).To(HaveField("Spec.Replicas", gomega.Equal(pointer.Int32(3))))
73+
// By calling the returned function directly it can also be used as gomega.Expect(k.Object(...)()).To(...)
74+
func Object(obj client.Object) func() (client.Object, error) {
75+
checkDefaultClient()
76+
return defaultK.Object(obj)
77+
}
78+
79+
// ObjectList returns a function that fetches a resource and returns the object.
80+
// It can be used with gomega.Eventually() like this:
81+
// deployments := appsv1.DeploymentList{ ... }
82+
// gomega.Eventually(k.ObjectList(&deployments)).To(HaveField("Items", HaveLen(1)))
83+
// By calling the returned function directly it can also be used as gomega.Expect(k.ObjectList(...)()).To(...)
84+
func ObjectList(list client.ObjectList, opts ...client.ListOption) func() (client.ObjectList, error) {
85+
checkDefaultClient()
86+
return defaultK.ObjectList(list, opts...)
87+
}

pkg/envtest/komega/default_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package komega
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/gomega"
7+
appsv1 "k8s.io/api/apps/v1"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/utils/pointer"
10+
)
11+
12+
func TestDefaultGet(t *testing.T) {
13+
g := NewWithT(t)
14+
15+
fc := createFakeClient()
16+
SetClient(fc)
17+
18+
fetched := appsv1.Deployment{
19+
ObjectMeta: metav1.ObjectMeta{
20+
Namespace: "default",
21+
Name: "test",
22+
},
23+
}
24+
g.Eventually(Get(&fetched)).Should(Succeed())
25+
26+
g.Expect(*fetched.Spec.Replicas).To(BeEquivalentTo(5))
27+
}
28+
29+
func TestDefaultList(t *testing.T) {
30+
g := NewWithT(t)
31+
32+
fc := createFakeClient()
33+
SetClient(fc)
34+
35+
list := appsv1.DeploymentList{}
36+
g.Eventually(List(&list)).Should(Succeed())
37+
38+
g.Expect(list.Items).To(HaveLen(1))
39+
depl := exampleDeployment()
40+
g.Expect(list.Items[0]).To(And(
41+
HaveField("ObjectMeta.Name", Equal(depl.ObjectMeta.Name)),
42+
HaveField("ObjectMeta.Namespace", Equal(depl.ObjectMeta.Namespace)),
43+
))
44+
}
45+
46+
func TestDefaultUpdate(t *testing.T) {
47+
g := NewWithT(t)
48+
49+
fc := createFakeClient()
50+
SetClient(fc)
51+
52+
updateDeployment := appsv1.Deployment{
53+
ObjectMeta: exampleDeployment().ObjectMeta,
54+
}
55+
g.Eventually(Update(&updateDeployment, func() {
56+
updateDeployment.Annotations = map[string]string{"updated": "true"}
57+
})).Should(Succeed())
58+
59+
fetched := appsv1.Deployment{
60+
ObjectMeta: exampleDeployment().ObjectMeta,
61+
}
62+
g.Expect(Object(&fetched)()).To(HaveField("ObjectMeta.Annotations", HaveKeyWithValue("updated", "true")))
63+
}
64+
65+
func TestDefaultUpdateStatus(t *testing.T) {
66+
g := NewWithT(t)
67+
68+
fc := createFakeClient()
69+
SetClient(fc)
70+
71+
updateDeployment := appsv1.Deployment{
72+
ObjectMeta: exampleDeployment().ObjectMeta,
73+
}
74+
g.Eventually(UpdateStatus(&updateDeployment, func() {
75+
updateDeployment.Status.AvailableReplicas = 1
76+
})).Should(Succeed())
77+
78+
fetched := appsv1.Deployment{
79+
ObjectMeta: exampleDeployment().ObjectMeta,
80+
}
81+
g.Expect(Object(&fetched)()).To(HaveField("Status.AvailableReplicas", BeEquivalentTo(1)))
82+
}
83+
84+
func TestDefaultObject(t *testing.T) {
85+
g := NewWithT(t)
86+
87+
fc := createFakeClient()
88+
SetClient(fc)
89+
90+
fetched := appsv1.Deployment{
91+
ObjectMeta: metav1.ObjectMeta{
92+
Namespace: "default",
93+
Name: "test",
94+
},
95+
}
96+
g.Eventually(Object(&fetched)).Should(And(
97+
Not(BeNil()),
98+
HaveField("Spec.Replicas", Equal(pointer.Int32(5))),
99+
))
100+
}
101+
102+
func TestDefaultObjectList(t *testing.T) {
103+
g := NewWithT(t)
104+
105+
fc := createFakeClient()
106+
SetClient(fc)
107+
108+
list := appsv1.DeploymentList{}
109+
g.Eventually(ObjectList(&list)).Should(And(
110+
Not(BeNil()),
111+
HaveField("Items", And(
112+
HaveLen(1),
113+
ContainElement(HaveField("Spec.Replicas", Equal(pointer.Int32(5)))),
114+
)),
115+
))
116+
}

pkg/envtest/komega/interfaces.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
Copyright 2021 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package komega
18+
19+
import (
20+
"context"
21+
22+
"sigs.k8s.io/controller-runtime/pkg/client"
23+
)
24+
25+
// Komega is a collection of utilites for writing tests involving a mocked
26+
// Kubernetes API.
27+
type Komega interface {
28+
// Get returns a function that fetches a resource and returns the occurring error.
29+
// It can be used with gomega.Eventually() like this
30+
// deployment := appsv1.Deployment{ ... }
31+
// gomega.Eventually(k.Get(&deployment)).To(gomega.Succeed())
32+
// By calling the returned function directly it can also be used with gomega.Expect(k.Get(...)()).To(...)
33+
Get(client.Object) func() error
34+
35+
// List returns a function that lists resources and returns the occurring error.
36+
// It can be used with gomega.Eventually() like this
37+
// deployments := v1.DeploymentList{ ... }
38+
// gomega.Eventually(k.List(&deployments)).To(gomega.Succeed())
39+
// By calling the returned function directly it can also be used as gomega.Expect(k.List(...)()).To(...)
40+
List(client.ObjectList, ...client.ListOption) func() error
41+
42+
// Update returns a function that fetches a resource, applies the provided update function and then updates the resource.
43+
// It can be used with gomega.Eventually() like this:
44+
// deployment := appsv1.Deployment{ ... }
45+
// gomega.Eventually(k.Update(&deployment, func (o client.Object) {
46+
// deployment.Spec.Replicas = 3
47+
// return &deployment
48+
// })).To(gomega.Scucceed())
49+
// By calling the returned function directly it can also be used as gomega.Expect(k.Update(...)()).To(...)
50+
Update(client.Object, func(), ...client.UpdateOption) func() error
51+
52+
// UpdateStatus returns a function that fetches a resource, applies the provided update function and then updates the resource's status.
53+
// It can be used with gomega.Eventually() like this:
54+
// deployment := appsv1.Deployment{ ... }
55+
// gomega.Eventually(k.Update(&deployment, func (o client.Object) {
56+
// deployment.Status.AvailableReplicas = 1
57+
// return &deployment
58+
// })).To(gomega.Scucceed())
59+
// By calling the returned function directly it can also be used as gomega.Expect(k.UpdateStatus(...)()).To(...)
60+
UpdateStatus(client.Object, func(), ...client.UpdateOption) func() error
61+
62+
// Object returns a function that fetches a resource and returns the object.
63+
// It can be used with gomega.Eventually() like this:
64+
// deployment := appsv1.Deployment{ ... }
65+
// gomega.Eventually(k.Object(&deployment)).To(HaveField("Spec.Replicas", gomega.Equal(pointer.Int32(3))))
66+
// By calling the returned function directly it can also be used as gomega.Expect(k.Object(...)()).To(...)
67+
Object(client.Object) func() (client.Object, error)
68+
69+
// ObjectList returns a function that fetches a resource and returns the object.
70+
// It can be used with gomega.Eventually() like this:
71+
// deployments := appsv1.DeploymentList{ ... }
72+
// gomega.Eventually(k.ObjectList(&deployments)).To(HaveField("Items", HaveLen(1)))
73+
// By calling the returned function directly it can also be used as gomega.Expect(k.ObjectList(...)()).To(...)
74+
ObjectList(client.ObjectList, ...client.ListOption) func() (client.ObjectList, error)
75+
76+
// WithContext returns a copy that uses the given context.
77+
WithContext(context.Context) Komega
78+
}

0 commit comments

Comments
 (0)