Implement Isolated, Deterministic Test Suite for ruletype CLI Subcommands#6286
Implement Isolated, Deterministic Test Suite for ruletype CLI Subcommands#6286DharunMR wants to merge 10 commits intomindersec:mainfrom
Conversation
39a2ab5 to
59733e1
Compare
evankanderson
left a comment
There was a problem hiding this comment.
This is very cool! A few comments -- I'd like to iterate on this a little bit, but I think you picked about the right "size" of example to catch interesting cases.
This seems a bit like WireMock, but I'm not sure pulling in that dependency is worthwhile.
| originalClientCreator := getRuleTypeClient | ||
| t.Cleanup(func() { getRuleTypeClient = originalClientCreator }) | ||
| getRuleTypeClient = func(_ grpc.ClientConnInterface) minderv1.RuleTypeServiceClient { | ||
| return mockClient | ||
| } |
There was a problem hiding this comment.
What do you think of making the function to get a typed proto interface a generic method which takes a context.Context? I think that would look like:
ctx := withRpcContext(context.Background, mockClient)
cmd.SetContext(ctx)And then the getter might look like:
func rpcContext[IF interface](ctx context.Context) IF {
if iface := ctx.Value(IF); iface != nil {
return iface
}
return getFullRpcStub(ctx)
}The main benefit would be that you wouldn't need to worry about the global getRuleTypeClient, but we could also simplify our current RunE -> cli.GRPCClientWrapRunE pattern to regular RunE functions.
(I hadn't thought about this or this pattern until tonight, and I find this testing idea interesting!)
There was a problem hiding this comment.
I completely agree that moving away from global state makes the tests much more robust. In the latest commit I’ve moved to this context approach.
| assert.Equal(t, string(expected), actual, "Output does not match golden file") | ||
| } | ||
|
|
||
| //nolint:unparam // filename is currently always the same, but kept generic for architectural consistency |
There was a problem hiding this comment.
This comment no longer appears to be correct.
There was a problem hiding this comment.
You're right, my previous comment was a bit unclear. Since make lint is still flagging this as unparam in this specific file, I've updated the comment to clarify that I'm keeping the signature generic for architectural consistency across the ruletype package's test suite
| mockSetup: func(t *testing.T, client *mockv1.MockRuleTypeServiceClient) { | ||
| t.Helper() | ||
| mockResp := &minderv1.ListRuleTypesResponse{} | ||
| loadFixture(t, "mock_ruletypes_response.json", mockResp) | ||
|
|
||
| client.EXPECT(). | ||
| GetRuleTypeById(gomock.Any(), gomock.Any()). | ||
| Return(&minderv1.GetRuleTypeByIdResponse{ | ||
| RuleType: mockResp.RuleTypes[1], | ||
| }, nil) | ||
|
|
||
| // simulate a failure (rule is in use) using the exact regex pattern the CLI expects | ||
| client.EXPECT(). | ||
| DeleteRuleType(gomock.Any(), gomock.Any()). | ||
| Return(nil, status.Error(codes.FailedPrecondition, "cannot delete: used by profiles my-security-profile")) | ||
| }, |
There was a problem hiding this comment.
It would be interesting if loadFixture could return a full RPC service, where the JSON included the function name for the responses, and then the mockSetup code could be replaced with rpcResponses: "mock_ruletypes_response.json".
There was a problem hiding this comment.
That is a really interesting idea moving to a json approach would definitely clean up the test structs and reduce the gomock boilerplate across the tests. I spent some time thinking about how we would actually implement that and I have two main thoughts based on my understanding
To make this work, we would need to build a dynamic dispatcher inside loadFixture to route those JSON method strings into actual gomock expectations. That is a pretty massive and feels a bit too complex
My other hesitation is when testing multiple edge cases the json fixtures would often be 95% identical, differing by only a few lines (e.g., just changing an error code). Burying those 2-3 meaningful lines inside massive duplicated json payloads actually makes the tests harder to read and PRs much harder to review.
This is based on my understanding so far. Could you let me know if I'm mistaken?
Signed-off-by: DharunMR <maddharun56@gmail.com>
Signed-off-by: DharunMR <maddharun56@gmail.com>
Signed-off-by: DharunMR <maddharun56@gmail.com>
Signed-off-by: DharunMR <maddharun56@gmail.com>
Signed-off-by: DharunMR <maddharun56@gmail.com>
Signed-off-by: DharunMR <maddharun56@gmail.com>
Signed-off-by: DharunMR <maddharun56@gmail.com>
Signed-off-by: DharunMR <maddharun56@gmail.com>
59733e1 to
7b33423
Compare
|
@evankanderson |
Summary
This PR introduces a robust, table driven testing architecture for the ruletype CLI command lifecycle (create, apply, get, list, delete). Historically, CLI commands can be brittle to test due to tightly coupled network calls and unpredictable standard output. This implementation establishes a new pilot pattern for CLI testing in Minder by enforcing strict network boundaries (via gRPC mocking) and deterministic output verification (via Golden Files and I/O interception).
Dependency Injection & Mocking Strategy
To isolate the CLI logic from the actual Minder control plane, this test suite leverages gomock to stub the RuleTypeServiceClient.
Deterministic Output Verification (Golden Files)
Testing CLI tools requires validating not just the exit codes, but the exact string formatting (Tables, JSON, YAML) presented to the user.
Testing
go test -v ./cmd/cli/app/ruletype(100% pass rate on new logic).make lintconfirming zero regressions or stylistic violations.Proposed Future Roadmap & Refactoring
If this isolated mock driven testing approach aligns with the maintainer's vision for the CLI, I propose the following follow-up work: