Skip to content

Commit f3b02f8

Browse files
committed
Migrate to fake client for OperatorSource unit tests
- Introduce Client interface as a raw client wrapper. Using the wrapper facilitates mocking of client interactions with the cluster, while using fakeclient during unit testing. - Refractor OperatorSource configuring reconciler code to use Client interface instead of raw client. - Update OperatorSource tests with fake client - Remove kubeclient mocks from generate-mocks target in Makefile
1 parent e274d6b commit f3b02f8

File tree

8 files changed

+139
-49
lines changed

8 files changed

+139
-49
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,11 @@ generate-mocks:
3434
@echo making sure directory for mocks exists
3535
mkdir -p $(MOCKS_DIR)
3636

37+
# $(mockgen) -destination=<Path/file where the mock is generated> -package=<The package that the generated mock files will belong to> -mock_names=<Original Interface name>=<Name of Generated mocked Interface> <Go package path of the original interface> <comma seperated list of the interface you want to mock>
3738
$(mockgen) -destination=$(MOCKS_DIR)/$(OPERATORSOURCE_MOCK_PKG)/mock_datastore.go -package=$(OPERATORSOURCE_MOCK_PKG) -mock_names=Reader=DatastoreReader,Writer=DatastoreWriter $(PKG)/datastore Reader,Writer
3839
$(mockgen) -destination=$(MOCKS_DIR)/$(OPERATORSOURCE_MOCK_PKG)/mock_phase_reconciler.go -package=$(OPERATORSOURCE_MOCK_PKG) -mock_names=Reconciler=PhaseReconciler $(PKG)/operatorsource Reconciler
3940
$(mockgen) -destination=$(MOCKS_DIR)/$(OPERATORSOURCE_MOCK_PKG)/mock_phase_tansitioner.go -package=$(OPERATORSOURCE_MOCK_PKG) -mock_names=Transitioner=PhaseTransitioner $(PKG)/phase Transitioner
40-
$(mockgen) -destination=$(MOCKS_DIR)/$(OPERATORSOURCE_MOCK_PKG)/mock_kubeclient.go -package=$(OPERATORSOURCE_MOCK_PKG) -mock_names=Client=KubeClient $(CONTROLLER_RUNTIME_PKG)/client Client
41+
$(mockgen) -destination=$(MOCKS_DIR)/$(OPERATORSOURCE_MOCK_PKG)/mock_client.go -package=$(OPERATORSOURCE_MOCK_PKG) -mock_names=Client=Client $(PKG)/client Client
4142
$(mockgen) -destination=$(MOCKS_DIR)/$(OPERATORSOURCE_MOCK_PKG)/mock_phase_reconciler_strategy.go -package=$(OPERATORSOURCE_MOCK_PKG) $(PKG)/operatorsource PhaseReconcilerFactory
4243
$(mockgen) -destination=$(MOCKS_DIR)/$(OPERATORSOURCE_MOCK_PKG)/mock_appregistry.go -package=$(OPERATORSOURCE_MOCK_PKG) -mock_names=ClientFactory=AppRegistryClientFactory,Client=AppRegistryClient $(PKG)/appregistry ClientFactory,Client
4344
$(mockgen) -destination=$(MOCKS_DIR)/$(OPERATORSOURCE_MOCK_PKG)/mock_syncer.go -package=$(OPERATORSOURCE_MOCK_PKG) -mock_names=PackageRefreshNotificationSender=SyncerPackageRefreshNotificationSender $(PKG)/operatorsource PackageRefreshNotificationSender

pkg/client/client.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package client
2+
3+
import (
4+
"context"
5+
6+
"k8s.io/apimachinery/pkg/runtime"
7+
"k8s.io/apimachinery/pkg/types"
8+
"sigs.k8s.io/controller-runtime/pkg/client"
9+
)
10+
11+
// ObjectKey identifies a Kubernetes Object.
12+
type ObjectKey = types.NamespacedName
13+
14+
// Client is a wrapper around the raw kube client provided
15+
// by operator-sdk. Using the wrapper facilitates mocking of client
16+
// interactions with the cluster, while using fakeclient during unit testing.
17+
type Client interface {
18+
Create(ctx context.Context, obj runtime.Object) error
19+
Get(ctx context.Context, key ObjectKey, objExisting runtime.Object) error
20+
Update(ctx context.Context, obj runtime.Object) error
21+
}
22+
23+
// kubeClient is an implementation of the Client interface
24+
type kubeClient struct {
25+
client client.Client
26+
}
27+
28+
// NewClient returns a kubeClient that can perform
29+
// create, get and update operations on a runtime object
30+
func NewClient(client client.Client) Client {
31+
return &kubeClient{
32+
client: client,
33+
}
34+
}
35+
36+
// Create creates a new runtime object in the cluster
37+
func (h *kubeClient) Create(ctx context.Context, obj runtime.Object) error {
38+
return h.client.Create(ctx, obj)
39+
}
40+
41+
// Get gets an existing runtime object from the cluster
42+
func (h *kubeClient) Get(ctx context.Context, key ObjectKey, objExisting runtime.Object) error {
43+
return h.client.Get(ctx, key, objExisting)
44+
}
45+
46+
// Update updates an existing runtime object in the cluster
47+
func (h *kubeClient) Update(ctx context.Context, obj runtime.Object) error {
48+
return h.client.Update(ctx, obj)
49+
}

pkg/operatorsource/configuring.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
marketplace "github.com/operator-framework/operator-marketplace/pkg/apis/operators/v1"
7+
interface_client "github.com/operator-framework/operator-marketplace/pkg/client"
78
"github.com/operator-framework/operator-marketplace/pkg/datastore"
89
"github.com/operator-framework/operator-marketplace/pkg/phase"
910
log "github.com/sirupsen/logrus"
@@ -15,6 +16,16 @@ import (
1516
// NewConfiguringReconciler returns a Reconciler that reconciles
1617
// an OperatorSource object in "Configuring" phase.
1718
func NewConfiguringReconciler(logger *log.Entry, datastore datastore.Writer, client client.Client) Reconciler {
19+
return NewReconcilerWithInterfaceClient(logger, datastore, interface_client.NewClient(client))
20+
}
21+
22+
// NewReconcilerWithInterfaceClient returns a configuring Reconciler
23+
// that reconciles an OperatorSource object in "Configuring" phase.
24+
// It uses the Client interface which is a wrapper to the raw client
25+
// provided by the operator-sdk, instead of the raw client itself.
26+
// Using this interface facilitates mocking of kube client interaction
27+
// with the cluster, while using fakeclient during unit testing.
28+
func NewReconcilerWithInterfaceClient(logger *log.Entry, datastore datastore.Writer, client interface_client.Client) Reconciler {
1829
return &configuringReconciler{
1930
logger: logger,
2031
datastore: datastore,
@@ -28,7 +39,7 @@ func NewConfiguringReconciler(logger *log.Entry, datastore datastore.Writer, cli
2839
type configuringReconciler struct {
2940
logger *log.Entry
3041
datastore datastore.Writer
31-
client client.Client
42+
client interface_client.Client
3243
builder *CatalogSourceConfigBuilder
3344
}
3445

pkg/operatorsource/configuring_test.go

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"github.com/operator-framework/operator-marketplace/pkg/operatorsource"
1414
"github.com/operator-framework/operator-marketplace/pkg/phase"
1515
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
16-
"k8s.io/apimachinery/pkg/types"
1716
)
1817

1918
// Use Case: Not configured, CatalogSourceConfig object has not been created yet.
@@ -29,9 +28,9 @@ func TestReconcile_NotConfigured_NewCatalogConfigSourceObjectCreated(t *testing.
2928
}
3029

3130
datastore := mocks.NewDatastoreWriter(controller)
32-
kubeclient := mocks.NewKubeClient(controller)
3331

34-
reconciler := operatorsource.NewConfiguringReconciler(helperGetContextLogger(), datastore, kubeclient)
32+
fakeclient := NewFakeClient()
33+
reconciler := operatorsource.NewConfiguringReconciler(helperGetContextLogger(), datastore, fakeclient)
3534

3635
ctx := context.TODO()
3736
opsrcIn := helperNewOperatorSourceWithPhase("marketplace", "foo", phase.Configuring)
@@ -51,7 +50,6 @@ func TestReconcile_NotConfigured_NewCatalogConfigSourceObjectCreated(t *testing.
5150
TargetNamespace: opsrcIn.Namespace,
5251
Packages: packages,
5352
}
54-
kubeclient.EXPECT().Create(context.TODO(), cscWant).Return(nil)
5553

5654
opsrcGot, nextPhaseGot, errGot := reconciler.Reconcile(ctx, opsrcIn)
5755

@@ -75,9 +73,6 @@ func TestReconcile_CatalogSourceConfigAlreadyExists_Updated(t *testing.T) {
7573
}
7674

7775
datastore := mocks.NewDatastoreWriter(controller)
78-
kubeclient := mocks.NewKubeClient(controller)
79-
80-
reconciler := operatorsource.NewConfiguringReconciler(helperGetContextLogger(), datastore, kubeclient)
8176

8277
ctx := context.TODO()
8378
opsrcIn := helperNewOperatorSourceWithPhase(namespace, name, phase.Configuring)
@@ -92,20 +87,10 @@ func TestReconcile_CatalogSourceConfigAlreadyExists_Updated(t *testing.T) {
9287
packages := "a,b,c"
9388
datastore.EXPECT().GetPackageIDsByOperatorSource(opsrcIn.GetUID()).Return(packages)
9489

95-
createErr := k8s_errors.NewAlreadyExists(schema.GroupResource{}, "CatalogSourceConfig already exists")
96-
kubeclient.EXPECT().Create(context.TODO(), gomock.Any()).Return(createErr)
97-
98-
// We expect Get to return the given CatalogSourceConfig successfully.
99-
namespacedName := types.NamespacedName{Name: name, Namespace: namespace}
100-
cscGet := marketplace.CatalogSourceConfig{}
101-
kubeclient.EXPECT().Get(context.TODO(), namespacedName, &cscGet).Return(nil)
90+
csc := helperNewCatalogSourceConfigWithLabels(namespace, name, labelsWant)
91+
fakeclient := NewFakeClientWithCSC(csc)
10292

103-
cscWant := helperNewCatalogSourceConfigWithLabels("", "", labelsWant)
104-
cscWant.Spec = marketplace.CatalogSourceConfigSpec{
105-
TargetNamespace: opsrcIn.Namespace,
106-
Packages: packages,
107-
}
108-
kubeclient.EXPECT().Update(context.TODO(), cscWant).Return(nil)
93+
reconciler := operatorsource.NewConfiguringReconciler(helperGetContextLogger(), datastore, fakeclient)
10994

11095
opsrcGot, nextPhaseGot, errGot := reconciler.Reconcile(ctx, opsrcIn)
11196

@@ -129,9 +114,9 @@ func TestReconcile_UpdateError_MovedToFailedPhase(t *testing.T) {
129114
}
130115

131116
datastore := mocks.NewDatastoreWriter(controller)
132-
kubeclient := mocks.NewKubeClient(controller)
117+
kubeclient := mocks.NewClient(controller)
133118

134-
reconciler := operatorsource.NewConfiguringReconciler(helperGetContextLogger(), datastore, kubeclient)
119+
reconciler := operatorsource.NewReconcilerWithInterfaceClient(helperGetContextLogger(), datastore, kubeclient)
135120

136121
ctx := context.TODO()
137122
opsrcIn := helperNewOperatorSourceWithPhase(namespace, name, phase.Configuring)

pkg/operatorsource/downloading_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ func TestReconcile_ScheduledForDownload_Success(t *testing.T) {
2929

3030
writer := mocks.NewDatastoreWriter(controller)
3131
factory := mocks.NewAppRegistryClientFactory(controller)
32-
kubeclient := mocks.NewKubeClient(controller)
32+
fakeclient := NewFakeClient()
3333
refresher := mocks.NewSyncerPackageRefreshNotificationSender(controller)
3434

35-
reconciler := operatorsource.NewDownloadingReconciler(helperGetContextLogger(), factory, writer, kubeclient, refresher)
35+
reconciler := operatorsource.NewDownloadingReconciler(helperGetContextLogger(), factory, writer, fakeclient, refresher)
3636

3737
ctx := context.TODO()
3838
opsrcIn := helperNewOperatorSourceWithPhase("marketplace", "foo", phase.OperatorSourceDownloading)
@@ -83,10 +83,10 @@ func TestReconcile_OperatorSourceReturnsEmptyManifestList_ErrorExpected(t *testi
8383

8484
writer := mocks.NewDatastoreWriter(controller)
8585
factory := mocks.NewAppRegistryClientFactory(controller)
86-
kubeclient := mocks.NewKubeClient(controller)
86+
fakeclient := NewFakeClient()
8787
refresher := mocks.NewSyncerPackageRefreshNotificationSender(controller)
8888

89-
reconciler := operatorsource.NewDownloadingReconciler(helperGetContextLogger(), factory, writer, kubeclient, refresher)
89+
reconciler := operatorsource.NewDownloadingReconciler(helperGetContextLogger(), factory, writer, fakeclient, refresher)
9090

9191
ctx := context.TODO()
9292
opsrcIn := helperNewOperatorSourceWithPhase("marketplace", "foo", phase.OperatorSourceDownloading)

pkg/operatorsource/handler_test.go

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/operator-framework/operator-marketplace/pkg/operatorsource"
1313
log "github.com/sirupsen/logrus"
1414
"github.com/stretchr/testify/assert"
15+
"k8s.io/apimachinery/pkg/types"
1516
"sigs.k8s.io/controller-runtime/pkg/client"
1617
)
1718

@@ -22,7 +23,10 @@ func TestHandle_PhaseHasChanged_UpdateExpected(t *testing.T) {
2223
controller := gomock.NewController(t)
2324
defer controller.Finish()
2425

25-
kubeclient := mocks.NewKubeClient(controller)
26+
// Making two OperatorSource objects that are not equal to simulate a change.
27+
opsrcIn, opsrcOut := helperNewOperatorSourceWithEndpoint("marketplace", "foo", "remote"), helperNewOperatorSourceWithEndpoint("marketplace", "foo", "local")
28+
29+
fakeclient := NewFakeClientWithOpsrc(opsrcIn)
2630
writer := mocks.NewDatastoreWriter(controller)
2731
factory := mocks.NewMockPhaseReconcilerFactory(controller)
2832
transitioner := mocks.NewPhaseTransitioner(controller)
@@ -32,13 +36,10 @@ func TestHandle_PhaseHasChanged_UpdateExpected(t *testing.T) {
3236
return cacheReconciler
3337
}
3438

35-
handler := operatorsource.NewHandlerWithParams(kubeclient, writer, factory, transitioner, newCacheReconcilerFunc)
39+
handler := operatorsource.NewHandlerWithParams(fakeclient, writer, factory, transitioner, newCacheReconcilerFunc)
3640

3741
ctx := context.TODO()
3842

39-
// Making two OperatorSource objects that are not equal to simulate a change.
40-
opsrcIn, opsrcOut := helperNewOperatorSourceWithEndpoint("marketplace", "foo", "remote"), helperNewOperatorSourceWithEndpoint("marketplace", "foo", "local")
41-
4243
phaseReconciler := mocks.NewPhaseReconciler(controller)
4344
factory.EXPECT().GetPhaseReconciler(gomock.Any(), opsrcIn).Return(phaseReconciler, nil).Times(1)
4445

@@ -55,12 +56,16 @@ func TestHandle_PhaseHasChanged_UpdateExpected(t *testing.T) {
5556
// We expect the transitioner to indicate that the object has changed and needs update.
5657
transitioner.EXPECT().TransitionInto(&opsrcOut.Status.CurrentPhase, nextPhaseExpected).Return(true).Times(1)
5758

58-
// We expect the object to be updated successfully.
59-
kubeclient.EXPECT().Update(ctx, opsrcOut).Return(nil).Times(1)
60-
6159
errGot := handler.Handle(ctx, opsrcIn)
6260

6361
assert.NoError(t, errGot)
62+
63+
// We expect the object to be updated successfully.
64+
namespacedName := types.NamespacedName{Name: "foo", Namespace: "marketplace"}
65+
opsrcGot := &marketplace.OperatorSource{}
66+
67+
fakeclient.Get(ctx, namespacedName, opsrcGot)
68+
assert.Equal(t, opsrcOut, opsrcGot)
6469
}
6570

6671
// Use Case: sdk passes an event with a valid object and reconciliation is
@@ -70,7 +75,11 @@ func TestHandle_PhaseHasNotChanged_NoUpdateExpected(t *testing.T) {
7075
controller := gomock.NewController(t)
7176
defer controller.Finish()
7277

73-
kubeclient := mocks.NewKubeClient(controller)
78+
// Making two OperatorSource objects that are not equal to simulate a change.
79+
opsrcIn, opsrcOut := helperNewOperatorSourceWithEndpoint("namespace", "foo", "local"), helperNewOperatorSourceWithEndpoint("namespace", "foo", "remote")
80+
81+
fakeclient := NewFakeClientWithOpsrc(opsrcIn)
82+
7483
writer := mocks.NewDatastoreWriter(controller)
7584
factory := mocks.NewMockPhaseReconcilerFactory(controller)
7685
transitioner := mocks.NewPhaseTransitioner(controller)
@@ -80,13 +89,10 @@ func TestHandle_PhaseHasNotChanged_NoUpdateExpected(t *testing.T) {
8089
return cacheReconciler
8190
}
8291

83-
handler := operatorsource.NewHandlerWithParams(kubeclient, writer, factory, transitioner, newCacheReconcilerFunc)
92+
handler := operatorsource.NewHandlerWithParams(fakeclient, writer, factory, transitioner, newCacheReconcilerFunc)
8493

8594
ctx := context.TODO()
8695

87-
// Making two OperatorSource objects that are not equal to simulate a change.
88-
opsrcIn, opsrcOut := helperNewOperatorSourceWithEndpoint("namespace", "foo", "local"), helperNewOperatorSourceWithEndpoint("namespace", "foo", "remote")
89-
9096
phaseReconciler := mocks.NewPhaseReconciler(controller)
9197
factory.EXPECT().GetPhaseReconciler(gomock.Any(), opsrcIn).Return(phaseReconciler, nil).Times(1)
9298

@@ -102,6 +108,13 @@ func TestHandle_PhaseHasNotChanged_NoUpdateExpected(t *testing.T) {
102108
errGot := handler.Handle(ctx, opsrcIn)
103109

104110
assert.NoError(t, errGot)
111+
112+
// We expect no changes to the object
113+
namespacedName := types.NamespacedName{Name: "foo", Namespace: "namespace"}
114+
opsrcGot := &marketplace.OperatorSource{}
115+
116+
fakeclient.Get(ctx, namespacedName, opsrcGot)
117+
assert.Equal(t, opsrcIn, opsrcGot)
105118
}
106119

107120
// Use Case: sdk passes an event with a valid object, reconciliation is not
@@ -111,7 +124,7 @@ func TestHandle_UpdateError_ReconciliationErrorReturned(t *testing.T) {
111124
controller := gomock.NewController(t)
112125
defer controller.Finish()
113126

114-
kubeclient := mocks.NewKubeClient(controller)
127+
fakeclient := NewFakeClient()
115128
writer := mocks.NewDatastoreWriter(controller)
116129
factory := mocks.NewMockPhaseReconcilerFactory(controller)
117130
transitioner := mocks.NewPhaseTransitioner(controller)
@@ -121,7 +134,7 @@ func TestHandle_UpdateError_ReconciliationErrorReturned(t *testing.T) {
121134
return cacheReconciler
122135
}
123136

124-
handler := operatorsource.NewHandlerWithParams(kubeclient, writer, factory, transitioner, newCacheReconcilerFunc)
137+
handler := operatorsource.NewHandlerWithParams(fakeclient, writer, factory, transitioner, newCacheReconcilerFunc)
125138

126139
ctx := context.TODO()
127140

@@ -144,12 +157,11 @@ func TestHandle_UpdateError_ReconciliationErrorReturned(t *testing.T) {
144157
// We expect transitioner to indicate that the object has been changed.
145158
transitioner.EXPECT().TransitionInto(&opsrcOut.Status.CurrentPhase, nextPhaseExpected).Return(true).Times(1)
146159

147-
// We expect the object to be updated
148-
updateErrorExpected := errors.New("object update error")
149-
kubeclient.EXPECT().Update(ctx, opsrcOut).Return(updateErrorExpected).Times(1)
150-
151160
errGot := handler.Handle(ctx, opsrcIn)
152161

153162
assert.Error(t, errGot)
154163
assert.Equal(t, reconcileErrorExpected, errGot)
164+
165+
// We expect the object to be updated
166+
assert.Error(t, fakeclient.Update(ctx, opsrcOut))
155167
}

pkg/operatorsource/helper_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,13 @@ package operatorsource_test
33
import (
44
"fmt"
55

6+
"github.com/operator-framework/operator-marketplace/pkg/apis"
67
marketplace "github.com/operator-framework/operator-marketplace/pkg/apis/operators/v1"
78
log "github.com/sirupsen/logrus"
89
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
10+
"k8s.io/apimachinery/pkg/runtime"
11+
"sigs.k8s.io/controller-runtime/pkg/client"
12+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
913
)
1014

1115
func helperGetContextLogger() *log.Entry {
@@ -88,3 +92,31 @@ func helperNewCatalogSourceConfigWithLabels(namespace, name string, opsrcLabels
8892

8993
return csc
9094
}
95+
96+
func NewFakeClient() client.Client {
97+
scheme := runtime.NewScheme()
98+
apis.AddToScheme(scheme)
99+
return fake.NewFakeClientWithScheme(scheme)
100+
}
101+
102+
func NewFakeClientWithCSC(csc *marketplace.CatalogSourceConfig) client.Client {
103+
objs := []runtime.Object{
104+
csc,
105+
}
106+
107+
scheme := runtime.NewScheme()
108+
apis.AddToScheme(scheme)
109+
110+
return fake.NewFakeClientWithScheme(scheme, objs...)
111+
}
112+
113+
func NewFakeClientWithOpsrc(opsrc *marketplace.OperatorSource) client.Client {
114+
scheme := runtime.NewScheme()
115+
apis.AddToScheme(scheme)
116+
117+
objs := []runtime.Object{
118+
opsrc,
119+
}
120+
121+
return fake.NewFakeClientWithScheme(scheme, objs...)
122+
}

pkg/operatorsource/purging_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ func TestReconcileWithPurging(t *testing.T) {
3131
}
3232

3333
datastore := mocks.NewDatastoreWriter(controller)
34-
client := mocks.NewKubeClient(controller)
35-
reconciler := operatorsource.NewPurgingReconciler(helperGetContextLogger(), datastore, client)
34+
fakeclient := NewFakeClient()
35+
reconciler := operatorsource.NewPurgingReconciler(helperGetContextLogger(), datastore, fakeclient)
3636

3737
// We expect the operator source to be removed from the datastore.
3838
datastore.EXPECT().RemoveOperatorSource(opsrcIn.GetUID()).Times(1)

0 commit comments

Comments
 (0)