openshift/assisted-service-writing-unit-tests
Use when writing non-subsystem tests in assisted-service.
npx skills add https://github.com/openshift/assisted-service --skill assisted-service-writing-unit-tests
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.
| 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() |
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"];
}
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.
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 verificationBeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
mockHandler = eventsapi.NewMockHandler(ctrl)
})
AfterEach(func() {
ctrl.Finish() // REQUIRED - verifies all EXPECT() constraints
})
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.
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) })
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
DescribeTable (simple cases):
DescribeTable("FunctionName",
func(input string, valid bool) { /* test logic */ },
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() { /* ... */ }) }
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()
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.
These thoughts mean STOP - fix the issue:
ctrl.Finish() in AfterEachgomock.NewController but no ctrl.Finish() → Silent failurescommon.TestVersion() outside subsystem/ → Wrong pattern; use hardcoded strings or common.TestDefaultConfigIf you're rationalizing shortcuts due to time pressure, the test will be broken. No exceptions.
| 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 |
Check every test file:
gomock.NewController has ctrl.Finish() in AfterEachBeforeEach)PrepareTestDB()TestVersion() usage — use hardcoded strings or common.TestDefaultConfiggo test -v ./path/to/packageRed flags: Missing ctrl.Finish() → silent failures. Shared variables → flaky tests.
Take openshift/assisted-service-writing-unit-tests from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.