github.com/mkimuram/operator-sdk@v0.7.1-0.20190410172100-52ad33a4bda0/doc/test-framework/writing-e2e-tests.md (about) 1 # Using the Operator SDK's Test Framework to Write E2E Tests 2 3 End-to-end tests are essential to ensure that an operator works 4 as intended in real-world scenarios. The Operator SDK includes a testing 5 framework to make writing tests simpler and quicker by removing boilerplate 6 code and providing common test utilities. The Operator SDK includes the 7 test framework as a library under `pkg/test` and the e2e tests are written 8 as standard go tests. 9 10 ## Components 11 12 The test framework includes a few components. The most important to talk 13 about are Framework and TestCtx. 14 15 ### Framework 16 17 [Framework][framework-link] contains all global variables, such as the kubeconfig, kubeclient, 18 scheme, and dynamic client (provided via the controller-runtime project). 19 It is initialized by MainEntry and can be used anywhere in the tests. 20 21 ### TestCtx 22 23 [TestCtx][testctx-link] is a local context that stores important information for each test, such 24 as the namespace for that test and the cleanup functions. By handling 25 namespace and resource initialization through TestCtx, we can make sure that all 26 resources are properly handled and removed after the test finishes. 27 28 ## Walkthrough: Writing Tests 29 30 In this section, we will be walking through writing the e2e tests of the sample 31 [memcached-operator][memcached-sample]. 32 33 ### Main Test 34 35 The first step to writing a test is to create the `main_test.go` file. The `main_test.go` 36 file simply calls the test framework's main entry that sets up the framework and then 37 starts the tests. It should be pretty much identical for all operators. This is what it 38 looks like for the memcached-operator: 39 40 ```go 41 package e2e 42 43 import ( 44 "testing" 45 46 f "github.com/operator-framework/operator-sdk/pkg/test" 47 ) 48 49 func TestMain(m *testing.M) { 50 f.MainEntry(m) 51 } 52 ``` 53 54 ### Individual Tests 55 56 In this section, we will be designing a test based on the [memcached_test.go][memcached-test-link] file 57 from the [memcached-operator][memcached-sample] sample. 58 59 #### 1. Import the framework 60 61 Once MainEntry sets up the framework, it runs the remainder of the tests. First, make 62 sure to import `testing`, the operator-sdk test framework (`pkg/test`) as well as your operator's libraries: 63 64 ```go 65 import ( 66 "testing" 67 68 cachev1alpha1 "github.com/operator-framework/operator-sdk-samples/memcached-operator/pkg/apis/cache/v1alpha1" 69 "github.com/operator-framework/operator-sdk-samples/memcached-operator/pkg/apis" 70 71 framework "github.com/operator-framework/operator-sdk/pkg/test" 72 ) 73 ``` 74 75 #### 2. Register types with framework scheme 76 77 The next step is to register your operator's scheme with the framework's dynamic client. 78 To do this, pass the CRD's `AddToScheme` function and its List type object to the framework's 79 [AddToFrameworkScheme][scheme-link] function. For our example memcached-operator, it looks like this: 80 81 ```go 82 memcachedList := &cachev1alpha1.MemcachedList{ 83 TypeMeta: metav1.TypeMeta{ 84 Kind: "Memcached", 85 APIVersion: "cache.example.com/v1alpha1", 86 }, 87 } 88 err := framework.AddToFrameworkScheme(apis.AddToScheme, memcachedList) 89 if err != nil { 90 t.Fatalf("failed to add custom resource scheme to framework: %v", err) 91 } 92 ``` 93 94 We pass in the CR List object `memcachedList` as an argument to `AddToFrameworkScheme()` because 95 the framework needs to ensure that the dynamic client has the REST mappings to query the API 96 server for the CR type. The framework will keep polling the API server for the mappings and 97 timeout after 5 seconds, returning an error if the mappings were not discovered in that time. 98 99 #### 3. Setup the test context and resources 100 101 The next step is to create a TestCtx for the current test and defer its cleanup function: 102 103 ```go 104 ctx := framework.NewTestCtx(t) 105 defer ctx.Cleanup() 106 ``` 107 108 Now that there is a `TestCtx`, the test's Kubernetes resources (specifically the test namespace, 109 Service Account, RBAC, and Operator deployment in `local` testing; just the Operator deployment 110 in `cluster` testing) can be initialized: 111 112 ```go 113 err := ctx.InitializeClusterResources(&framework.CleanupOptions{TestContext: ctx, Timeout: cleanupTimeout, RetryInterval: cleanupRetryInterval}) 114 if err != nil { 115 t.Fatalf("failed to initialize cluster resources: %v", err) 116 } 117 ``` 118 119 The `InitializeClusterResources` function uses the custom `Create` function in the framework client to create the resources provided 120 in your namespaced manifest. The custom `Create` function use the controller-runtime's client to create resources and then 121 creates a cleanup function that is called by `ctx.Cleanup` which deletes the resource and then waits for the resource to be 122 fully deleted before returning. This is configurable with `CleanupOptions`. For info on how to use `CleanupOptions` see 123 [this section](#how-to-use-cleanup). 124 125 If you want to make sure the operator's deployment is fully ready before moving onto the next part of the 126 test, the `WaitForOperatorDeployment` function from [e2eutil][e2eutil-link] (in the sdk under `pkg/test/e2eutil`) can be used: 127 128 ```go 129 // get namespace 130 namespace, err := ctx.GetNamespace() 131 if err != nil { 132 t.Fatal(err) 133 } 134 // get global framework variables 135 f := framework.Global 136 // wait for memcached-operator to be ready 137 err = e2eutil.WaitForOperatorDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, time.Second*5, time.Second*30) 138 if err != nil { 139 t.Fatal(err) 140 } 141 ``` 142 143 #### 4. Write the test specific code 144 145 Since the controller-runtime's dynamic client uses go contexts, make sure to import the go context library. 146 In this example, we imported it as `goctx`: 147 148 ##### <a id="how-to-use-cleanup"></a>How to use the Framework Client `Create`'s `CleanupOptions` 149 150 The test framework provides `Client`, which exposes most of the controller-runtime's client unmodified, but the `Create` 151 function has added functionality to create cleanup functions for these resources as well. To manage how cleanup 152 is handled, we use a `CleanupOptions` struct. Here are some examples of how to use it: 153 154 ```go 155 // Create with no cleanup 156 Create(goctx.TODO(), exampleMemcached, &framework.CleanupOptions{}) 157 Create(goctx.TODO(), exampleMemcached, nil) 158 159 // Create with cleanup but no polling for resources to be deleted 160 Create(goctx.TODO(), exampleMemcached, &framework.CleanupOptions{TestContext: ctx}) 161 162 // Create with cleanup and polling wait for resources to be deleted 163 Create(goctx.TODO(), exampleMemcached, &framework.CleanupOptions{TestContext: ctx, Timeout: timeout, RetryInterval: retryInterval}) 164 ``` 165 166 This is how we can create a custom memcached custom resource with a size of 3: 167 168 ```go 169 // create memcached custom resource 170 exampleMemcached := &cachev1alpha1.Memcached{ 171 TypeMeta: metav1.TypeMeta{ 172 Kind: "Memcached", 173 APIVersion: "cache.example.com/v1alpha1", 174 }, 175 ObjectMeta: metav1.ObjectMeta{ 176 Name: "example-memcached", 177 Namespace: namespace, 178 }, 179 Spec: cachev1alpha1.MemcachedSpec{ 180 Size: 3, 181 }, 182 } 183 err = f.Client.Create(goctx.TODO(), exampleMemcached, &framework.CleanupOptions{TestContext: ctx, Timeout: time.Second * 5, RetryInterval: time.Second * 1}) 184 if err != nil { 185 return err 186 } 187 ``` 188 189 Now we can check if the operator successfully worked. In the case of the memcached operator, it should have 190 created a deployment called "example-memcached" with 3 replicas. To check, we use the `WaitForDeployment` function, which 191 is the same as `WaitForOperatorDeployment` with the exception that `WaitForOperatorDeployment` will skip waiting 192 for the deployment if the test is run locally and the `--up-local` flag is set; the `WaitForDeployment` function always 193 waits for the deployment: 194 195 ```go 196 // wait for example-memcached to reach 3 replicas 197 err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "example-memcached", 3, time.Second*5, time.Second*30) 198 if err != nil { 199 return err 200 } 201 ``` 202 203 We can also test that the deployment scales correctly when the CR is updated: 204 205 ```go 206 err = f.Client.Get(goctx.TODO(), types.NamespacedName{Name: "example-memcached", Namespace: namespace}, exampleMemcached) 207 if err != nil { 208 return err 209 } 210 exampleMemcached.Spec.Size = 4 211 err = f.Client.Update(goctx.TODO(), exampleMemcached) 212 if err != nil { 213 return err 214 } 215 216 // wait for example-memcached to reach 4 replicas 217 err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "example-memcached", 4, time.Second*5, time.Second*30) 218 if err != nil { 219 return err 220 } 221 ``` 222 223 Once the end of the function is reached, the TestCtx's cleanup 224 functions will automatically be run since they were deferred when the TestCtx was created. 225 226 ## Running the Tests 227 228 To make running the tests simpler, the `operator-sdk` CLI tool has a `test` subcommand that can configure 229 default test settings, such as locations of your global resource manifest file (by default 230 `deploy/crd.yaml`) and your namespaced resource manifest file (by default `deploy/service_account.yaml` concatenated with 231 `deploy/rbac.yaml` and `deploy/operator.yaml`), and allows the user to configure runtime options. There are 2 ways to use the 232 subcommand: local and cluster. 233 234 ### Local 235 236 To run the tests locally, run the `operator-sdk test local` command in your project root and pass the location of the tests 237 as an argument. You can use `--help` to view the other configuration options and use `--go-test-flags` to pass in arguments to `go test`. Here is an example command: 238 239 ```shell 240 $ operator-sdk test local ./test/e2e --go-test-flags "-v -parallel=2" 241 ``` 242 243 #### Image Flag 244 245 If you wish to specify a different operator image than specified in your `operator.yaml` file (or a user-specified 246 namespaced manifest file), you can use the `--image` flag: 247 248 ```shell 249 $ operator-sdk test local ./test/e2e --image quay.io/example/my-operator:v0.0.2 250 ``` 251 252 #### Namespace Flag 253 254 If you wish to run all the tests in 1 namespace (which also forces `-parallel=1`), you can use the `--namespace` flag: 255 256 ```shell 257 $ kubectl create namespace operator-test 258 $ operator-sdk test local ./test/e2e --namespace operator-test 259 ``` 260 261 #### Up-Local Flag 262 263 To run the operator itself locally during the tests instead of starting a deployment in the cluster, you can use the 264 `--up-local` flag. This mode will still create global resources, but by default will not create any in-cluster namespaced 265 resources unless the user specifies one through the `--namespaced-manifest` flag. (Note: the `--up-local` flag requires 266 the `--namespace` flag): 267 268 ```shell 269 $ kubectl create namespace operator-test 270 $ operator-sdk test local ./test/e2e --namespace operator-test --up-local 271 ``` 272 273 #### No-Setup Flag 274 275 If you would prefer to create the resources yourself and skip resource creation, you can use the `--no-setup` flag: 276 ```shell 277 $ kubectl create namespace operator-test 278 $ kubectl create -f deploy/crds/cache_v1alpha1_memcached_crd.yaml 279 $ kubectl create -f deploy/service_account.yaml --namespace operator-test 280 $ kubectl create -f deploy/role.yaml --namespace operator-test 281 $ kubectl create -f deploy/role_binding.yaml --namespace operator-test 282 $ kubectl create -f deploy/operator.yaml --namespace operator-test 283 $ operator-sdk test local ./test/e2e --namespace operator-test --no-setup 284 ``` 285 286 For more documentation on the `operator-sdk test local` command, see the [SDK CLI Reference][sdk-cli-ref] doc. 287 288 #### Running Go Test Directly (Not Recommended) 289 290 For advanced use cases, it is possible to run the tests via `go test` directly. As long as all flags defined 291 in [MainEntry][main-entry-link] are declared, the tests will run correctly. Running the tests directly with missing flags 292 will result in undefined behavior. This is an example `go test` equivalent to the `operator-sdk test local` example above: 293 294 ```shell 295 # Combine service_account, rbac, operator manifest into namespaced manifest 296 $ cp deploy/service_account.yaml deploy/namespace-init.yaml 297 $ echo -e "\n---\n" >> deploy/namespace-init.yaml 298 $ cat deploy/rbac.yaml >> deploy/namespace-init.yaml 299 $ echo -e "\n---\n" >> deploy/namespace-init.yaml 300 $ cat deploy/operator.yaml >> deploy/namespace-init.yaml 301 # Run tests 302 $ go test ./test/e2e/... -root=$(pwd) -kubeconfig=$HOME/.kube/config -globalMan deploy/crd.yaml -namespacedMan deploy/namespace-init.yaml -v -parallel=2 303 ``` 304 305 ### Cluster 306 307 Another way to run the tests is from within a Kubernetes cluster. To do this, you first need to build an image with 308 the testing binary embedded by using the `operator-sdk build` command and using the `--enable-tests` flag to enable tests: 309 310 ```shell 311 $ operator-sdk build quay.io/example/memcached-operator:v0.0.1 --enable-tests 312 ``` 313 314 Note that the namespaced yaml must be up to date before running this command. The `build` subcommand will warn you 315 if it finds a deployment in the namespaced manifest with an image that doesn't match the argument you provided. The 316 `operator-sdk build` command has other flags for configuring the tests that can be viewed with the `--help` flag 317 or at the [SDK CLI Reference][sdk-cli-ref]. 318 319 Once the image is ready, the tests are ready to be run. To run the tests, make sure you have all global resources 320 and a namespace with proper rbac configured: 321 322 ```shell 323 $ kubectl create -f deploy/crds/cache_v1alpha1_memcached_crd.yaml 324 $ kubectl create namespace memcached-test 325 $ kubectl create -f deploy/service_account.yaml -n memcached-test 326 $ kubectl create -f deploy/role.yaml -n memcached-test 327 $ kubectl create -f deploy/role_binding.yaml -n memcached-test 328 ``` 329 330 Once you have your environment properly configured, you can start the tests using the `operator-sdk test cluster` command: 331 332 ```shell 333 $ operator-sdk test cluster quay.io/example/memcached-operator:v0.0.1 --namespace memcached-test --service-account memcached-operator 334 335 Example Output: 336 Test Successfully Completed 337 ``` 338 339 The `test cluster` command will deploy a test pod in the given namespace that will run the e2e tests packaged in the image. 340 The tests run sequentially in the namespace (`-parallel=1`), the same as running `operator-sdk test local --namespace <namespace>`. 341 The command will wait until the tests succeed (pod phase=`Succeeded`) or fail (pod phase=`Failed`). 342 If the tests fail, the command will output the test pod logs which should be the standard go test error logs. 343 344 ## Manual Cleanup 345 346 While the test framework provides utilities that allow the test to automatically be cleaned up when done, 347 it is possible that an error in the test code could cause a panic, which would stop the test 348 without running the deferred cleanup. To clean up manually, you should check what namespaces currently exist 349 in your cluster. You can do this with `kubectl`: 350 351 ```shell 352 $ kubectl get namespaces 353 354 Example Output: 355 NAME STATUS AGE 356 default Active 2h 357 kube-public Active 2h 358 kube-system Active 2h 359 main-1534287036 Active 23s 360 memcached-memcached-group-cluster-1534287037 Active 22s 361 memcached-memcached-group-cluster2-1534287037 Active 22s 362 ``` 363 364 The names of the namespaces will be either start with `main` or with the name of the tests and the suffix will 365 be a Unix timestamp (number of seconds since January 1, 1970 00:00 UTC). Kubectl can be used to delete these 366 namespaces and the resources in those namespaces: 367 368 ```shell 369 $ kubectl delete namespace main-153428703 370 ``` 371 372 Since the CRD is not namespaced, it must be deleted separately. Clean up the CRD created by the tests using the CRD manifest `deploy/crd.yaml`: 373 374 ```shell 375 $ kubectl delete -f deploy/crds/cache_v1alpha1_memcached_crd.yaml 376 ``` 377 378 [memcached-sample]:https://github.com/operator-framework/operator-sdk-samples/tree/master/memcached-operator 379 [framework-link]:https://github.com/operator-framework/operator-sdk/blob/master/pkg/test/framework.go#L45 380 [testctx-link]:https://github.com/operator-framework/operator-sdk/blob/master/pkg/test/context.go 381 [e2eutil-link]:https://github.com/operator-framework/operator-sdk/tree/master/pkg/test/e2eutil 382 [memcached-test-link]:https://github.com/operator-framework/operator-sdk-samples/blob/master/memcached-operator/test/e2e/memcached_test.go 383 [scheme-link]:https://github.com/operator-framework/operator-sdk/blob/master/pkg/test/framework.go#L109 384 [sdk-cli-ref]:https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#test 385 [main-entry-link]:https://github.com/operator-framework/operator-sdk/blob/master/pkg/test/main_entry.go#L25