|
| 1 | +--- |
| 2 | +name: assisted-service-writing-unit-tests |
| 3 | +description: Use when writing unit tests in assisted-service with gomock mocks, Ginkgo/Gomega, events, or database. Critical when tests pass but mocks aren't verified, or gomock.NewController appears in code. |
| 4 | +--- |
| 5 | + |
| 6 | +# Writing Unit Tests in Assisted-Service |
| 7 | + |
| 8 | +## Overview |
| 9 | + |
| 10 | +Unit tests in assisted-service use Ginkgo/Gomega BDD framework with gomock for mocking. **Core principle: Every gomock controller MUST call `ctrl.Finish()` in `AfterEach` to verify mock expectations.** |
| 11 | + |
| 12 | +Without `ctrl.Finish()`, tests pass even when mocks aren't called correctly - silent failures that hide bugs. Tests run against real PostgreSQL (suite or per-test) with automatic cleanup. |
| 13 | + |
| 14 | +## Quick Reference |
| 15 | + |
| 16 | +| Scenario | Pattern | |
| 17 | +|----------|---------| |
| 18 | +| Testing with mocks | Create controller in `BeforeEach`, call `ctrl.Finish()` in `AfterEach` | |
| 19 | +| Testing without mocks | Use `It()` blocks directly or table-driven tests | |
| 20 | +| Database per test | `PrepareTestDB()` in `BeforeEach`, `DeleteTestDB()` in `AfterEach` | |
| 21 | +| Database for suite | `InitializeDBTest()` in `BeforeSuite`, `TerminateDBTest()` in `AfterSuite` | |
| 22 | +| Event verification | `eventstest.NewEventMatcher` with matchers | |
| 23 | + |
| 24 | +## When to Use Patterns |
| 25 | + |
| 26 | +```dot |
| 27 | +digraph mock_decision { |
| 28 | + "Testing interface deps?" [shape=diamond]; |
| 29 | + "Need verify calls or control returns?" [shape=diamond]; |
| 30 | + "Use gomock mocks" [shape=box]; |
| 31 | + "Use real implementation" [shape=box]; |
| 32 | +
|
| 33 | + "Testing interface deps?" -> "Need verify calls or control returns?" [label="yes"]; |
| 34 | + "Testing interface deps?" -> "Use real implementation" [label="no"]; |
| 35 | + "Need verify calls or control returns?" -> "Use gomock mocks" [label="yes"]; |
| 36 | + "Need verify calls or control returns?" -> "Use real implementation" [label="no"]; |
| 37 | +} |
| 38 | +``` |
| 39 | + |
| 40 | +```dot |
| 41 | +digraph db_decision { |
| 42 | + "Tests share data?" [shape=diamond]; |
| 43 | + "Tests modify DB state?" [shape=diamond]; |
| 44 | + "Suite-level DB" [shape=box]; |
| 45 | + "Per-test DB" [shape=box]; |
| 46 | +
|
| 47 | + "Tests share data?" -> "Suite-level DB" [label="yes"]; |
| 48 | + "Tests share data?" -> "Tests modify DB state?" [label="no"]; |
| 49 | + "Tests modify DB state?" -> "Per-test DB" [label="yes"]; |
| 50 | + "Tests modify DB state?" -> "Suite-level DB" [label="no"]; |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +## Scope |
| 55 | + |
| 56 | +**Covers:** Unit tests in `internal/` and `pkg/` with gomock, Ginkgo/Gomega, database, events. |
| 57 | + |
| 58 | +**Does NOT cover:** Subsystem tests, E2E, external service integration, performance tests. |
| 59 | + |
| 60 | +**Test files:** `*_test.go` (same package). Suites: `*_suite_test.go`. |
| 61 | + |
| 62 | +## Critical: Gomock Controllers MUST Call Finish() |
| 63 | + |
| 64 | +**Every `Describe`/`Context` creating a gomock controller needs `ctrl.Finish()` in `AfterEach`. No exceptions.** |
| 65 | + |
| 66 | +Without `ctrl.Finish()`: |
| 67 | +- `.Times(N)` - Expected call count NOT checked |
| 68 | +- `.MaxTimes(0)` - Unexpected calls NOT caught |
| 69 | +- `.Do()` / `.DoAndReturn()` - Mock behavior runs without verification |
| 70 | + |
| 71 | +```go |
| 72 | +BeforeEach(func() { |
| 73 | + ctrl = gomock.NewController(GinkgoT()) |
| 74 | + mockHandler = eventsapi.NewMockHandler(ctrl) |
| 75 | +}) |
| 76 | + |
| 77 | +AfterEach(func() { |
| 78 | + ctrl.Finish() // REQUIRED - verifies all EXPECT() constraints |
| 79 | +}) |
| 80 | +``` |
| 81 | + |
| 82 | +**Why this matters:** Missing `ctrl.Finish()` causes silent test failures - tests pass even when mocks aren't called correctly. Real bugs found in commit `338133e05 MGMT-23548`. |
| 83 | + |
| 84 | +**Letter = Spirit:** Following the pattern exactly IS following the spirit. This isn't ritual - it's how gomock verification works. |
| 85 | + |
| 86 | +## Database Setup Patterns |
| 87 | + |
| 88 | +**Suite-level** (shared DB, read-only tests): |
| 89 | +```go |
| 90 | +BeforeSuite(func() { common.InitializeDBTest() }) |
| 91 | +AfterSuite(func() { common.TerminateDBTest() }) |
| 92 | +``` |
| 93 | + |
| 94 | +**Per-test** (isolated DB, tests modify state): |
| 95 | +```go |
| 96 | +BeforeEach(func() { db, dbName = common.PrepareTestDB() }) |
| 97 | +AfterEach(func() { common.DeleteTestDB(db, dbName) }) |
| 98 | +``` |
| 99 | + |
| 100 | +## Event Testing |
| 101 | + |
| 102 | +Use `eventstest.NewEventMatcher` with specific matchers: |
| 103 | + |
| 104 | +```go |
| 105 | +mockEvents.EXPECT().SendHostEvent(gomock.Any(), eventstest.NewEventMatcher( |
| 106 | + eventstest.WithNameMatcher(eventgen.HostStatusUpdatedEventName), |
| 107 | + eventstest.WithHostIdMatcher(host.ID.String()))) |
| 108 | +``` |
| 109 | + |
| 110 | +**Matchers:** `WithNameMatcher`, `WithHostIdMatcher`, `WithClusterIdMatcher`, `WithInfraEnvIdMatcher`, `WithSeverityMatcher` |
| 111 | + |
| 112 | +## Table-Driven Tests |
| 113 | + |
| 114 | +**DescribeTable** (simple cases): |
| 115 | +```go |
| 116 | +DescribeTable("FunctionName", |
| 117 | + func(input string, valid bool) { /* test logic */ }, |
| 118 | + Entry("descriptive case 1", "value1", true), |
| 119 | + Entry("descriptive case 2", "value2", false)) |
| 120 | +``` |
| 121 | + |
| 122 | +**Struct array** (complex scenarios): |
| 123 | +```go |
| 124 | +tests := []struct{name, input string; valid bool}{ |
| 125 | + {name: "case 1", input: "val1", valid: true}} |
| 126 | +for _, t := range tests { It(t.name, func() { /* ... */ }) } |
| 127 | +``` |
| 128 | + |
| 129 | +## Test Structure |
| 130 | + |
| 131 | +Ginkgo hierarchy: `Describe("Component")` → `Context("when X")` → `It("should Y")` |
| 132 | + |
| 133 | +**Isolation:** No shared state between `It` blocks. Use `BeforeEach` for setup. |
| 134 | + |
| 135 | +**Gomock matchers:** `.Times(N)`, `.MaxTimes(0)`, `.AnyTimes()`, `gomock.Any()` |
| 136 | + |
| 137 | +## Red Flags - Stop and Fix |
| 138 | + |
| 139 | +These thoughts mean STOP - fix the issue: |
| 140 | + |
| 141 | +- "Tests are passing, mocks must be working" → Add `ctrl.Finish()` in `AfterEach` |
| 142 | +- "Too complex to set up properly" → Follow patterns, don't skip structure |
| 143 | +- "Minimum viable test to satisfy review" → Tests must verify behavior, not theater |
| 144 | +- "I'll add cleanup/verification later" → Do it now, tests will merge broken |
| 145 | +- Code has `gomock.NewController` but no `ctrl.Finish()` → Silent failures |
| 146 | + |
| 147 | +**If you're rationalizing shortcuts due to time pressure, the test will be broken. No exceptions.** |
| 148 | + |
| 149 | +## Common Mistakes |
| 150 | + |
| 151 | +| Mistake | Symptom | Fix | |
| 152 | +|---------|---------|-----| |
| 153 | +| Missing `ctrl.Finish()` | Tests pass when mocks not called | Add `ctrl.Finish()` in `AfterEach` | |
| 154 | +| Shared variables between tests | Flaky tests, race conditions | Move initialization to `BeforeEach` | |
| 155 | +| Missing `GinkgoT()` | Controller doesn't report failures | Use `gomock.NewController(GinkgoT())` | |
| 156 | +| Generic Entry names | "test 1", "test 2" in output | Descriptive: "valid IPv4 CIDR" | |
| 157 | +| Wrong DB pattern | Pollution between tests | Suite-level for reads, per-test for writes | |
| 158 | +| No event matchers | Generic `gomock.Any()` for events | Use `eventstest.NewEventMatcher` with specific matchers | |
| 159 | + |
| 160 | +## Before Submitting |
| 161 | + |
| 162 | +**Check every test file:** |
| 163 | +- [ ] Every `gomock.NewController` has `ctrl.Finish()` in `AfterEach` |
| 164 | +- [ ] No shared state (all setup in `BeforeEach`) |
| 165 | +- [ ] Descriptive names ("valid IPv4 CIDR" not "test 1") |
| 166 | +- [ ] Database cleanup if using `PrepareTestDB()` |
| 167 | +- [ ] Tests pass: `go test -v ./path/to/package` |
| 168 | + |
| 169 | +**Red flags:** Missing `ctrl.Finish()` → silent failures. Shared variables → flaky tests. |
0 commit comments