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