| name | assisted-service-writing-unit-tests |
| description | Use when writing non-subsystem tests in assisted-service. |
Writing Unit Tests in Assisted-Service
Overview
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.
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.
Quick Reference
| Scenario | Pattern |
|---|
| Testing with mocks | Create controller in BeforeEach, call ctrl.Finish() in AfterEach |
| Testing without mocks | Use It() blocks directly or table-driven tests |
| Database per test | PrepareTestDB() in BeforeEach, DeleteTestDB() in AfterEach |
| Database for suite | InitializeDBTest() in BeforeSuite, TerminateDBTest() in AfterSuite |
| Event verification | eventstest.NewEventMatcher with matchers |
| OCP version in tests | Use hardcoded strings or common.TestDefaultConfig — do not use TestVersion() |
When to Use Patterns
digraph mock_decision {
"Testing interface deps?" [shape=diamond];
"Need verify calls or control returns?" [shape=diamond];
"Use gomock mocks" [shape=box];
"Use real implementation" [shape=box];
"Testing interface deps?" -> "Need verify calls or control returns?" [label="yes"];
"Testing interface deps?" -> "Use real implementation" [label="no"];
"Need verify calls or control returns?" -> "Use gomock mocks" [label="yes"];
"Need verify calls or control returns?" -> "Use real implementation" [label="no"];
}
digraph db_decision {
"Tests share data?" [shape=diamond];
"Tests modify DB state?" [shape=diamond];
"Suite-level DB" [shape=box];
"Per-test DB" [shape=box];
"Tests share data?" -> "Suite-level DB" [label="yes"];
"Tests share data?" -> "Tests modify DB state?" [label="no"];
"Tests modify DB state?" -> "Per-test DB" [label="yes"];
"Tests modify DB state?" -> "Suite-level DB" [label="no"];
}
Scope
Covers: Non-subsystem tests in internal/ and pkg/ with gomock, Ginkgo/Gomega, database, events.
Does NOT cover: Subsystem tests (subsystem/), E2E, external service integration, performance tests. Subsystem tests use different patterns, including common.TestVersion() for OCP versions.
Test files: *_test.go (same package). Suites: *_suite_test.go.
Critical: Gomock Controllers MUST Call Finish()
Every Describe/Context creating a gomock controller needs ctrl.Finish() in AfterEach. No exceptions.
Without ctrl.Finish():
.Times(N) - Expected call count NOT checked
.MaxTimes(0) - Unexpected calls NOT caught
.Do() / .DoAndReturn() - Mock behavior runs without verification
BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
mockHandler = eventsapi.NewMockHandler(ctrl)
})
AfterEach(func() {
ctrl.Finish()
})
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.
Letter = Spirit: Following the pattern exactly IS following the spirit. This isn't ritual - it's how gomock verification works.
Database Setup Patterns
Suite-level (shared DB, read-only tests):
BeforeSuite(func() { common.InitializeDBTest() })
AfterSuite(func() { common.TerminateDBTest() })
Per-test (isolated DB, tests modify state):
BeforeEach(func() { db, dbName = common.PrepareTestDB() })
AfterEach(func() { common.DeleteTestDB(db, dbName) })
Event Testing
Use eventstest.NewEventMatcher with specific matchers:
mockEvents.EXPECT().SendHostEvent(gomock.Any(), eventstest.NewEventMatcher(
eventstest.WithNameMatcher(eventgen.HostStatusUpdatedEventName),
eventstest.WithHostIdMatcher(host.ID.String())))
Matchers: WithNameMatcher, WithHostIdMatcher, WithClusterIdMatcher, WithInfraEnvIdMatcher, WithSeverityMatcher
Table-Driven Tests
DescribeTable (simple cases):
DescribeTable("FunctionName",
func(input string, valid bool) { },
Entry("descriptive case 1", "value1", true),
Entry("descriptive case 2", "value2", false))
Struct array (complex scenarios):
tests := []struct{name, input string; valid bool}{
{name: "case 1", input: "val1", valid: true}}
for _, t := range tests { It(t.name, func() { }) }
Test Structure
Ginkgo hierarchy: Describe("Component") → Context("when X") → It("should Y")
Isolation: No shared state between It blocks. Use BeforeEach for setup.
Gomock matchers: .Times(N), .MaxTimes(0), .AnyTimes(), gomock.Any()
OCP Versions: Use Hardcoded Strings, Not TestVersion()
Do not use common.TestVersion() in non-subsystem tests. TestVersion() resolves versions dynamically from data files and is designed for subsystem tests (subsystem/), which run against the real service. Non-subsystem tests mock version data and should use hardcoded version strings directly.
Correct patterns:
OpenshiftVersion: swag.String("4.14"),
Or use the package-level defaults from internal/common/test_configuration.go:
OpenshiftVersion: swag.String(common.OpenShiftVersion),
ReleaseVersion: common.ReleaseVersion,
ReleaseImageUrl: common.ReleaseImageURL,
See docs/dev/test-versions.md for the TestVersion() API used in subsystem tests.
Red Flags - Stop and Fix
These thoughts mean STOP - fix the issue:
- "Tests are passing, mocks must be working" → Add
ctrl.Finish() in AfterEach
- "Too complex to set up properly" → Follow patterns, don't skip structure
- "Minimum viable test to satisfy review" → Tests must verify behavior, not theater
- "I'll add cleanup/verification later" → Do it now, tests will merge broken
- Code has
gomock.NewController but no ctrl.Finish() → Silent failures
- Using
common.TestVersion() outside subsystem/ → Wrong pattern; use hardcoded strings or common.TestDefaultConfig
If you're rationalizing shortcuts due to time pressure, the test will be broken. No exceptions.
Common Mistakes
| Mistake | Symptom | Fix |
|---|
Missing ctrl.Finish() | Tests pass when mocks not called | Add ctrl.Finish() in AfterEach |
| Shared variables between tests | Flaky tests, race conditions | Move initialization to BeforeEach |
Missing GinkgoT() | Controller doesn't report failures | Use gomock.NewController(GinkgoT()) |
| Generic Entry names | "test 1", "test 2" in output | Descriptive: "valid IPv4 CIDR" |
| Wrong DB pattern | Pollution between tests | Suite-level for reads, per-test for writes |
| No event matchers | Generic gomock.Any() for events | Use eventstest.NewEventMatcher with specific matchers |
Using TestVersion() | Wrong abstraction for non-subsystem tests | Use hardcoded strings or common.TestDefaultConfig |
Before Submitting
Check every test file:
Red flags: Missing ctrl.Finish() → silent failures. Shared variables → flaky tests.