Skip to content

Commit

Permalink
test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
Brian Mendoza committed Apr 24, 2024
1 parent ea505af commit 3341a63
Show file tree
Hide file tree
Showing 9 changed files with 155 additions and 56 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ docs:

.PHONY: test
test: generate fmt vet envtest ## Run tests.
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(CACHE_BIN) -p path)" go test -race -timeout 60s `go list ./... | grep -v ./mock$` -coverprofile cover.out.tmp
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(CACHE_BIN) -p path)" go test -race -timeout 60s `go list ./... | grep -v ./mock$$` -coverprofile cover.out.tmp
grep -v "zz_generated.deepcopy.go" cover.out.tmp > cover.out
rm cover.out.tmp

Expand Down
6 changes: 3 additions & 3 deletions cloud/scope/machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func TestValidateMachineScopeParams(t *testing.T) {
func TestMachineScopeAddFinalizer(t *testing.T) {
t.Parallel()

NewTestSuite(t, mock.MockK8sClient{}).Run(Paths(
NewSuite(t, mock.MockK8sClient{}).Run(Paths(
Call("scheme 1", func(ctx context.Context, mck Mock) {
mck.K8sClient.EXPECT().Scheme().DoAndReturn(func() *runtime.Scheme {
s := runtime.NewScheme()
Expand Down Expand Up @@ -189,7 +189,7 @@ func TestMachineScopeAddFinalizer(t *testing.T) {
func TestNewMachineScope(t *testing.T) {
t.Parallel()

NewTestSuite(t, mock.MockK8sClient{}).Run(Paths(
NewSuite(t, mock.MockK8sClient{}).Run(Paths(
Either(
Result("invalid params", func(ctx context.Context, mck Mock) {
mScope, err := NewMachineScope(ctx, "token", MachineScopeParams{})
Expand Down Expand Up @@ -324,7 +324,7 @@ func TestNewMachineScope(t *testing.T) {
func TestMachineScopeGetBootstrapData(t *testing.T) {
t.Parallel()

NewTestSuite(t, mock.MockK8sClient{}).Run(Paths(
NewSuite(t, mock.MockK8sClient{}).Run(Paths(
Call("able to get secret", func(ctx context.Context, mck Mock) {
mck.K8sClient.EXPECT().Get(ctx, gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, key client.ObjectKey, obj *corev1.Secret, opts ...client.GetOption) error {
Expand Down
6 changes: 3 additions & 3 deletions controller/linodeobjectstoragebucket_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ type accessKeySecret struct {
}

var _ = Describe("lifecycle", Ordered, Label("bucket", "lifecycle"), func() {
suite := NewControllerTestSuite(GinkgoT(), mock.MockLinodeObjectStorageClient{})
suite := NewControllerSuite(GinkgoT(), mock.MockLinodeObjectStorageClient{})

obj := infrav1.LinodeObjectStorageBucket{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -87,7 +87,7 @@ var _ = Describe("lifecycle", Ordered, Label("bucket", "lifecycle"), func() {
Recorder: suite.Recorder(),
}

suite.BeforeAll(func(ctx context.Context, _ Mock) {
BeforeAll(func(ctx SpecContext) {
bScope.Client = k8sClient
Expect(k8sClient.Create(ctx, &obj)).To(Succeed())
})
Expand Down Expand Up @@ -359,7 +359,7 @@ var _ = Describe("lifecycle", Ordered, Label("bucket", "lifecycle"), func() {
})

var _ = Describe("errors", Label("bucket", "errors"), func() {
suite := NewControllerTestSuite(
suite := NewControllerSuite(
GinkgoT(),
mock.MockLinodeObjectStorageClient{},
mock.MockK8sClient{},
Expand Down
8 changes: 3 additions & 5 deletions docs/src/developers/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ While writing test cases for each scenario, we'd likely find a lot of overlap be

```go
func TestEnsureInstanceNotOffline(t *testing.T) {
suite := NewTestSuite(t, mock.MockLinodeMachineClient{})
suite := NewSuite(t, mock.MockLinodeMachineClient{})

suite.Run(t, Paths(
Either(
Expand Down Expand Up @@ -104,9 +104,7 @@ In this example, the nodes passed into `Paths` are used to describe each permuta
* `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.

#### Setup, tear down, and event triggers
Setup and teardown nodes can be scheduled before and after each run:
* `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.
* `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.
Setup and tear down nodes can be scheduled before and after each run. `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.

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.

Expand Down Expand Up @@ -138,7 +136,7 @@ CAPL uses controller-runtime's [envtest](https://book.kubebuilder.io/reference/e
// This is needed when relying on EnvTest for managing Kubernetes API server state.
var _ = Describe("test name", Ordered, func() {
// Create a mocktest controller test suite.
suite := NewControllerTestSuite(GinkgoT(), mock.MockLinodeMachineClient{})
suite := NewControllerSuite(GinkgoT(), mock.MockLinodeMachineClient{})

obj := infrav1alpha1.LinodeMachine{
ObjectMeta: metav1.ObjectMeta{/* ... */}
Expand Down
2 changes: 0 additions & 2 deletions mock/mocktest/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import (
type Mock struct {
gomock.TestReporter
mock.MockClients

endOfPath bool
}

// Common interface for defining permutations of test paths as a tree.
Expand Down
3 changes: 0 additions & 3 deletions mock/mocktest/path_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ func (p path) Run(ctx context.Context, mck Mock) {
evalFn(ctx, mck, fn(c))
}

mckPtr := &mck
mckPtr.endOfPath = true

evalFn(ctx, mck, fn(p.result))
}

Expand Down
45 changes: 39 additions & 6 deletions mock/mocktest/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,17 @@ var _ = Describe("k8s client", Label("k8sclient"), func() {
mockCtrl.Finish()
})

for _, path := range Paths(
for _, pth := range Paths(
Once("setup", func(_ context.Context, _ Mock) {}),
Call("fetch object", func(ctx context.Context, mck Mock) {
mck.K8sClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
}),
Result("no error", func(ctx context.Context, mck Mock) {
Expect(contrivedCalls(ctx, mck)).To(Succeed())
}),
) {
It(path.Describe(), func(ctx SpecContext) {
path.Run(ctx, Mock{
It(pth.Describe(), func(ctx SpecContext) {
pth.Run(ctx, Mock{
TestReporter: GinkgoT(),
MockClients: mock.MockClients{
K8sClient: mock.NewMockK8sClient(mockCtrl),
Expand All @@ -65,7 +66,7 @@ var _ = Describe("multiple clients", Label("multiple"), func() {
mockCtrl.Finish()
})

for _, path := range Paths(
for _, pth := range Paths(
Call("read object", func(ctx context.Context, mck Mock) {
mck.K8sClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
}),
Expand All @@ -88,8 +89,8 @@ var _ = Describe("multiple clients", Label("multiple"), func() {
),
),
) {
It(path.Describe(), func(ctx SpecContext) {
path.Run(ctx, Mock{
It(pth.Describe(), func(ctx SpecContext) {
pth.Run(ctx, Mock{
TestReporter: GinkgoT(),
MockClients: mock.MockClients{
MachineClient: mock.NewMockLinodeMachineClient(mockCtrl),
Expand Down Expand Up @@ -128,6 +129,12 @@ func TestPaths(t *testing.T) {
describe []string
panic bool
}{
{
name: "empty",
input: []node{},
output: nil,
describe: []string{},
},
{
name: "basic",
input: []node{
Expand Down Expand Up @@ -552,3 +559,29 @@ func TestPaths(t *testing.T) {
})
}
}

func TestRunWithoutTestReporter(t *testing.T) {
t.Parallel()

pth := path{}
assert.Panics(t, func() {
pth.Run(context.Background(), Mock{})
})
}

func TestEvalOnceOnlyCallsOnce(t *testing.T) {
t.Parallel()

var toggle bool

onceFn := once{does: func(_ context.Context, _ Mock) {
toggle = !toggle
}}

ctx := context.Background()
mck := Mock{}
evalOnce(ctx, mck, &onceFn)
evalOnce(ctx, mck, &onceFn)

assert.True(t, toggle)
}
47 changes: 14 additions & 33 deletions mock/mocktest/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ type suite struct {
clients []mock.MockClient
beforeEach []fn
afterEach []fn
beforeAll []*once
afterAll []*once
}

func (s *suite) BeforeEach(action func(context.Context, Mock)) {
Expand All @@ -38,20 +36,6 @@ func (s *suite) AfterEach(action func(context.Context, Mock)) {
})
}

func (s *suite) BeforeAll(action func(context.Context, Mock)) {
s.beforeAll = append(s.beforeAll, &once{
text: "BeforeAll",
does: action,
})
}

func (s *suite) AfterAll(action func(context.Context, Mock)) {
s.afterAll = append(s.afterAll, &once{
text: "AfterAll",
does: action,
})
}

func (s *suite) run(t gomock.TestHelper, ctx context.Context, pth path) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
Expand All @@ -64,9 +48,6 @@ func (s *suite) run(t gomock.TestHelper, ctx context.Context, pth path) {
mck.MockClients.Build(client, mockCtrl)
}

for _, fun := range s.beforeAll {
evalOnce(ctx, mck, fun)
}
for _, fun := range s.beforeEach {
evalFn(ctx, mck, fun)
}
Expand All @@ -76,9 +57,6 @@ func (s *suite) run(t gomock.TestHelper, ctx context.Context, pth path) {
for _, fun := range s.afterEach {
evalFn(ctx, mck, fun)
}
for _, fun := range s.afterAll {
evalOnce(ctx, mck, fun)
}
}

type standardSuite struct {
Expand All @@ -87,7 +65,9 @@ type standardSuite struct {
t *testing.T
}

func NewTestSuite(t *testing.T, clients ...mock.MockClient) *standardSuite {
// NewSuite creates a test suite using Go's standard testing library.
// It generates new mock clients for each test path it runs.
func NewSuite(t *testing.T, clients ...mock.MockClient) *standardSuite {
t.Helper()

if len(clients) == 0 {
Expand All @@ -100,12 +80,13 @@ func NewTestSuite(t *testing.T, clients ...mock.MockClient) *standardSuite {
}
}

func (ss *standardSuite) Run(paths []path) {
for _, path := range paths {
ss.t.Run(path.Describe(), func(t *testing.T) {
// Run calls t.Run for each computed test path.
func (ss *standardSuite) Run(pths []path) {
for _, pth := range pths {
ss.t.Run(pth.Describe(), func(t *testing.T) {
t.Parallel()

ss.suite.run(t, context.Background(), path)
ss.suite.run(t, context.Background(), pth)
})
}
}
Expand All @@ -122,9 +103,9 @@ type ctlrSuite struct {
logs *bytes.Buffer
}

// NewControllerTestSuite creates a test suite for a controller.
// NewControllerSuite creates a test suite for a controller.
// It generates new mock clients for each test path it runs.
func NewControllerTestSuite(ginkgoT ginkgo.FullGinkgoTInterface, clients ...mock.MockClient) *ctlrSuite {
func NewControllerSuite(ginkgoT ginkgo.FullGinkgoTInterface, clients ...mock.MockClient) *ctlrSuite {
if len(clients) == 0 {
panic(errors.New("unable to run tests without clients"))
}
Expand Down Expand Up @@ -173,10 +154,10 @@ func (cs *ctlrSuite) Logs() string {

// Run executes Ginkgo test specs for each computed test path.
// It manages mock client instantiation, events, and logging.
func (cs *ctlrSuite) Run(paths []path) {
for _, path := range paths {
ginkgo.It(path.Describe(), func(ctx ginkgo.SpecContext) {
cs.suite.run(cs.ginkgoT, ctx, path)
func (cs *ctlrSuite) Run(pths []path) {
for _, pth := range pths {
ginkgo.It(pth.Describe(), func(ctx ginkgo.SpecContext) {
cs.suite.run(cs.ginkgoT, ctx, pth)

Check warning on line 160 in mock/mocktest/suite.go

View check run for this annotation

Codecov / codecov/patch

mock/mocktest/suite.go#L157-L160

Added lines #L157 - L160 were not covered by tests

// Flush the channel if any events were not consumed
// i.e. Events was never called
Expand Down
92 changes: 92 additions & 0 deletions mock/mocktest/suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package mocktest

import (
"context"
"errors"
"sync"
"testing"
"time"

"github.com/onsi/ginkgo/v2"
"github.com/stretchr/testify/assert"

"github.com/linode/cluster-api-provider-linode/mock"
)

func TestSuitesNoClients(t *testing.T) {
t.Parallel()

assert.Panics(t, func() { NewSuite(t) })
assert.Panics(t, func() { NewControllerSuite(ginkgo.GinkgoT()) })
}

func TestSuite(t *testing.T) {
t.Parallel()

//nolint:paralleltest // these tests should run prior to their nested t.Run
for _, testCase := range []struct {
name string
beforeEach, afterEach bool
expectedCount int
}{
{
name: "beforeEach",
beforeEach: true,
expectedCount: 6,
},
{
name: "afterEach",
afterEach: true,
expectedCount: 6,
},
{
name: "both",
beforeEach: true,
afterEach: true,
expectedCount: 8,
},
} {
t.Run(testCase.name, func(t *testing.T) {
// Create a counter with the expected number of calls.
// As each call runs, the counter will decrement to 0.
var counter sync.WaitGroup
counter.Add(testCase.expectedCount)

suite := NewSuite(t, mock.MockK8sClient{})
if testCase.beforeEach {
suite.BeforeEach(func(_ context.Context, _ Mock) { counter.Done() })
}
if testCase.afterEach {
suite.AfterEach(func(_ context.Context, _ Mock) { counter.Done() })
}

suite.Run(Paths(
Either(
Call("", func(_ context.Context, _ Mock) { counter.Done() }),
Call("", func(_ context.Context, _ Mock) { counter.Done() }),
),
Result("", func(_ context.Context, _ Mock) { counter.Done() }),
))

// Wait until the counter reaches 0, or time out.
// This runs in a goroutine so the nested t.Runs are scheduled.
go func() {
select {
case <-waitCh(&counter):
return
case <-time.NewTimer(time.Second * 5).C:
assert.Error(t, errors.New(testCase.name))
}
}()
})
}
}

func waitCh(counter *sync.WaitGroup) <-chan struct{} {
out := make(chan struct{})
go func() {
counter.Wait()
out <- struct{}{}
}()
return out
}

0 comments on commit 3341a63

Please sign in to comment.