You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/src/developers/testing.md
+171Lines changed: 171 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,177 @@ In order to run the unit tests run the following command
6
6
```bash
7
7
make test
8
8
```
9
+
### Adding tests
10
+
General unit tests of functions follow the same conventions for testing using Go's `testing` standard library, along with the [testify](https://github.com/stretchr/testify) toolkit for making assertions.
11
+
12
+
Unit tests that require API clients use mock clients generated using [gomock](https://github.com/uber-go/mock). To simplify the usage of mock clients, this repo also uses an internal library defined in `mock/mocktest`.
13
+
14
+
`mocktest` is usually imported as a dot import along with the `mock` package:
Using `mocktest` involves creating a test suite that specifies the mock clients to be used within each test scope and running the test suite using a DSL for defnining test nodes belong to one or more `Paths`.
25
+
26
+
#### Example
27
+
The following is a contrived example using the mock Linode machine client.
28
+
29
+
Let's say we've written an idempotent function `EnsureInstanceRuns` that 1) gets an instance or creates it if it doesn't exist, 2) boots the instance if it's offline. Testing this function would mean we'd need to write test cases for all permutations, i.e.
30
+
* instance exists and is not offline
31
+
* instance exists but is offline, and is able to boot
32
+
* instance exists but is offline, and is not able to boot
33
+
* instance does not exist, and is not able to be created
34
+
* instance does not exist, and is able to be created, and is able to boot
35
+
* instance does not exist, and is able to be created, and is not able to boot
36
+
37
+
While writing test cases for each scenario, we'd likely find a lot of overlap between each. `mocktest` provides a DSL for defining each unique test case without needing to spell out all required mock client calls for each case. Here's how we could test `EnsureInstanceRuns` using `mocktest`:
require.ErrorContains(t, err, "instance was not booted: boot failed: reasons...")
91
+
assert.Empty(inst)
92
+
})
93
+
)
94
+
),
95
+
)
96
+
}
97
+
```
98
+
In this example, the nodes passed into `Paths` are used to describe each permutation of the function being called with different results from the mock Linode machine client.
99
+
100
+
#### Nodes
101
+
*`Call` describes the behavior of method calls by mock clients. A `Call` node can belong to one or more paths.
102
+
*`Result` invokes the function with mock clients and tests the output. A `Result` node terminates each path it belongs to.
103
+
*`Path` is a list of nodes that all belong to the same test path. Each child node of a `Path` is evaluated in order.
104
+
*`Either` is a list of nodes that all belong to different test paths. It is used to define diverging test path, with each path containing the set of all preceding `Call` nodes.
105
+
106
+
#### Setup, tear down, and event triggers
107
+
Setup and teardown nodes can be scheduled before and after each run:
108
+
*`suite.BeforeEach` receives a `func(context.Context, Mock)` function that will run before each path is evaluated. Likewise, `suite.AfterEach` will run after each path is evaluated.
109
+
*`suite.BeforeAll` receives a `func(context.Context, Mock)` function taht will run once before all paths are evaluated. Likewise, `suite.AfterEach` will run after each path is evaluated.
110
+
111
+
In addition to the path nodes listed in the section above, a special node type `Once` may be specified to inject a function that will only be evaluated one time across all paths. It can be used to trigger side effects outside of mock client behavior that can impact the output of the function being tested.
112
+
113
+
#### Control flow
114
+
When `Run` is called on a test suite, paths are evaluated in parallel using `t.Parallel()`. Each path will be run with a separate `t.Run` call, and each test run will be named according to the descriptions specified in each node.
115
+
116
+
To help with visualizing the paths that will be rendered from nodes, a `Describe` helper method can be called which returns a slice of strings describing each path. For instance, the following shows the output of `Describe` on the paths described in the example above:
117
+
118
+
```go
119
+
paths:=Paths(/* see example above */)
120
+
121
+
paths.Describe() /* [
122
+
"instance exists and is not offline > success",
123
+
"instance does not exist > not able to be created > error",
124
+
"instance does not exist > able to be created > able to boot > success",
125
+
"instance does not exist > able to be created > not able to boot > error",
126
+
"instance exists but is offline > able to boot > success",
127
+
"instance exists but is offline > not able to boot > error"
128
+
] */
129
+
```
130
+
131
+
#### Testing controllers
132
+
CAPL uses controller-runtime's [envtest](https://book.kubebuilder.io/reference/envtest) package which runs an instance of etcd and the Kubernetes API server for testing controllers. The test setup uses [ginkgo](https://onsi.github.io/ginkgo/) as its test runner as well as [gomega](https://onsi.github.io/gomega/) for assertions.
133
+
134
+
`mocktest` is also recommended when writing tests for controllers. The following is another contrived example of how to use it within the context of a Ginkgo `Describe` node:
135
+
136
+
```go
137
+
// Add the `Ordered` decorator so that tests run in order versus in parallel.
138
+
// This is needed when relying on EnvTest for managing Kubernetes API server state.
0 commit comments