-
Notifications
You must be signed in to change notification settings - Fork 41
Add testcase for CLI #565
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add testcase for CLI #565
Conversation
Signed-off-by: HunterChen <1480811935cyt@gmail.com>
Signed-off-by: HunterChen <1480811935cyt@gmail.com>
Signed-off-by: HunterChen <1480811935cyt@gmail.com>
|
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: The full list of commands accepted by this bot can be found here. DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
Summary of ChangesHello @huntersman, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the robustness and maintainability of the CLI by introducing extensive unit tests for its core command functionalities. It also includes strategic refactoring to improve testability, such as enabling client mocking and configurable template paths, alongside minor code quality improvements. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request significantly improves the project's quality by adding a comprehensive suite of unit tests for the CLI commands. The refactoring to enable mocking of the Kubernetes client is a commendable change that follows best practices for testability. I've identified one critical issue regarding a syntax error in a test data file and a couple of medium-severity issues related to unhandled errors in test helper functions. Addressing these points will further strengthen the reliability of the new tests.
cli/kthena/cmd/testdata/helm/templates/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B.yaml
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive unit tests for the CLI command package and introduces refactoring to improve testability. The changes enable better testing coverage for core command functionalities and set up infrastructure for reliable CLI testing.
- Introduces test infrastructure with embedded test data and command execution helpers
- Adds extensive test coverage for create, describe, get, and root commands
- Refactors code for improved testability through dependency injection patterns
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| cli/kthena/main.go | Removes placeholder copyright comment block |
| cli/kthena/cmd/root.go | Adds missing fmt import and improves error handling in Execute function by printing errors before exit |
| cli/kthena/cmd/get.go | Refactors getKthenaClient to use function variable for test mocking |
| cli/kthena/cmd/templates.go | Makes templatesBasePath configurable to support test data directories |
| cli/kthena/cmd/common_test.go | Adds test infrastructure with embedded test data and command execution helper |
| cli/kthena/cmd/create_test.go | Adds comprehensive tests for create manifest command including flag handling and template rendering |
| cli/kthena/cmd/describe_test.go | Adds tests for describe command with template and resource description scenarios |
| cli/kthena/cmd/get_test.go | Adds tests for get commands including templates, model-boosters, and various resource types |
| cli/kthena/cmd/root_test.go | Adds tests for root command, subcommands, and command aliases |
| cli/kthena/cmd/templates_test.go | Adds tests for template listing, existence checking, and content retrieval |
| cli/kthena/cmd/testdata/helm/templates/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B.yaml | Adds test template for DeepSeek R1 Distill Qwen 7B model |
| cli/kthena/cmd/testdata/helm/templates/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B.yaml | Adds test template for DeepSeek R1 Distill Qwen 32B model |
| cli/kthena/cmd/testdata/helm/templates/Qwen/Qwen3-8B.yaml | Adds test template for Qwen3 8B model |
| cli/kthena/cmd/testdata/helm/templates/Qwen/Qwen3-32B.yaml | Adds test template for Qwen3 32B model |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cli/kthena/cmd/testdata/helm/templates/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B.yaml
Outdated
Show resolved
Hide resolved
Signed-off-by: HunterChen <1480811935cyt@gmail.com>
…ration Signed-off-by: HunterChen <1480811935cyt@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as outdated.
This comment was marked as outdated.
E2E tests have limited resources, so they can't run the CLI models. |
|
@huntersman we can test it using a fake image, just check the pod status? |
| // getKthenaClientFunc is a variable that can be mocked in tests | ||
| var getKthenaClientFunc = getKthenaClientImpl | ||
|
|
||
| func getKthenaClient() (versioned.Interface, error) { | ||
| return getKthenaClientFunc() | ||
| } | ||
|
|
||
| func getKthenaClientImpl() (versioned.Interface, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Your current approach works, but it's not the best practice. It uses global variable injection which has several drawbacks. Let me show you better alternatives:
Problems with Global Variable Injection:
- Test pollution: Tests can interfere with each other
- Concurrency issues: Can't run tests in parallel
- Hidden dependencies: Hard to track what's being mocked
- Reset complexity: Need careful
defercleanup - Not thread-safe: Dangerous in concurrent code
Better Practice: Dependency Injection Patterns
Pattern 1: Constructor Injection (Recommended)
// cmd/get.go
type GetCommand struct {
client versioned.Interface
// other dependencies
}
// Constructor accepts dependencies
func NewGetCommand(client versioned.Interface) *GetCommand {
return &GetCommand{client: client}
}
func (c *GetCommand) Run(args []string) error {
// Use c.client directly
boosters, err := c.client.WorkloadV1alpha1().ModelBoosters("").List(context.Background(), metav1.ListOptions{})
// ...
}
// In main.go
func main() {
client, _ := getRealClient()
cmd := NewGetCommand(client)
cmd.Run(os.Args[1:])
}
// In tests
func TestGetCommand(t *testing.T) {
fakeClient := fake.NewClientset(testData...)
cmd := NewGetCommand(fakeClient)
// Test cmd.Run()
}Pattern 2: Interface-based Factory
// internal/client/factory.go
type ClientFactory interface {
GetClient() (versioned.Interface, error)
}
type RealClientFactory struct{}
func (f *RealClientFactory) GetClient() (versioned.Interface, error) {
config, err := clientcmd.BuildConfigFromFlags("", clientcmd.RecommendedHomeFile)
if err != nil {
return nil, err
}
return versioned.NewForConfig(config)
}
// In commands
type GetCommand struct {
clientFactory ClientFactory
}
func NewGetCommand(factory ClientFactory) *GetCommand {
return &GetCommand{clientFactory: factory}
}
func (c *GetCommand) Run(args []string) error {
client, err := c.clientFactory.GetClient()
if err != nil {
return err
}
// Use client...
}
// Mock factory for tests
type MockClientFactory struct {
Client versioned.Interface
Err error
}
func (m *MockClientFactory) GetClient() (versioned.Interface, error) {
return m.Client, m.Err
}
// Test
func TestGetCommand(t *testing.T) {
fakeClient := fake.NewClientset(testData...)
factory := &MockClientFactory{Client: fakeClient}
cmd := NewGetCommand(factory)
// Test...
}Pattern 3: Functional Options (Most Flexible)
// cmd/options.go
type CommandOption func(*CommandConfig)
type CommandConfig struct {
Client versioned.Interface
Output io.Writer
// other config
}
func WithClient(client versioned.Interface) CommandOption {
return func(c *CommandConfig) {
c.Client = client
}
}
func WithOutput(w io.Writer) CommandOption {
return func(c *CommandConfig) {
c.Output = w
}
}
// cmd/get.go
func NewGetCommand(opts ...CommandOption) *GetCommand {
config := &CommandConfig{
Output: os.Stdout, // default
}
for _, opt := range opts {
opt(config)
}
return &GetCommand{
client: config.Client,
output: config.Output,
}
}
// Usage in production
func main() {
client, _ := getRealClient()
cmd := NewGetCommand(
WithClient(client),
WithOutput(os.Stdout),
)
}
// Usage in tests
func TestGetCommand(t *testing.T) {
fakeClient := fake.NewClientset(testData...)
var buf bytes.Buffer
cmd := NewGetCommand(
WithClient(fakeClient),
WithOutput(&buf),
)
// Test and inspect buf
}Pattern 4: Context/Request-scoped Injection
// internal/context/context.go
type RequestContext struct {
Client versioned.Interface
// other request-scoped dependencies
}
// Commands accept context
func RunGetCommand(ctx *RequestContext, args []string) error {
boosters, err := ctx.Client.WorkloadV1alpha1().ModelBoosters("").List(context.Background(), metav1.ListOptions{})
// ...
}
// In main
func main() {
client, _ := getRealClient()
ctx := &RequestContext{Client: client}
RunGetCommand(ctx, os.Args[1:])
}
// In tests
func TestGetCommand(t *testing.T) {
fakeClient := fake.NewClientset(testData...)
ctx := &RequestContext{Client: fakeClient}
err := RunGetCommand(ctx, []string{"get", "modelboosters"})
// assert...
}Refactoring Your Current Code:
From This (Current):
// ❌ Global variable approach
var getKthenaClientFunc = getKthenaClientImpl
func getModelBoosters() error {
client, err := getKthenaClientFunc() // Implicit dependency
// ...
}To This (Better):
// ✅ Explicit dependency
type ModelBoosterService struct {
Client versioned.Interface
}
func NewModelBoosterService(client versioned.Interface) *ModelBoosterService {
return &ModelBoosterService{Client: client}
}
func (s *ModelBoosterService) ListBoosters(namespace string) ([]workloadv1alpha1.ModelBooster, error) {
boosters, err := s.Client.WorkloadV1alpha1().ModelBoosters(namespace).List(context.Background(), metav1.ListOptions{})
// ...
}
// Command uses service
type GetModelBoostersCommand struct {
service *ModelBoosterService
}
func NewGetModelBoostersCommand(service *ModelBoosterService) *GetModelBoostersCommand {
return &GetModelBoostersCommand{service: service}
}
func (c *GetModelBoostersCommand) Run(args []string) error {
boosters, err := c.service.ListBoosters("")
// Format and output...
}Complete Example with Cobra (Common for Go CLIs):
// cmd/root.go
package cmd
import (
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
)
// Root command holds shared dependencies
type RootCmd struct {
*cobra.Command
k8sClient kubernetes.Interface
}
func NewRootCmd(client kubernetes.Interface) *RootCmd {
root := &RootCmd{
Command: &cobra.Command{
Use: "mycli",
Short: "My Kubernetes CLI",
},
k8sClient: client,
}
// Add subcommands with dependencies
root.AddCommand(NewGetCmd(root.k8sClient))
root.AddCommand(NewApplyCmd(root.k8sClient))
return root
}
// cmd/get.go
func NewGetCmd(client kubernetes.Interface) *cobra.Command {
return &cobra.Command{
Use: "get [resource]",
Short: "Get resources",
RunE: func(cmd *cobra.Command, args []string) error {
// Client is available via closure
pods, err := client.CoreV1().Pods("").List(cmd.Context(), metav1.ListOptions{})
// ...
return nil
},
}
}
// main.go
func main() {
client := createRealClient()
rootCmd := cmd.NewRootCmd(client)
rootCmd.Execute()
}
// tests/cmd/get_test.go
func TestGetCommand(t *testing.T) {
fakeClient := fake.NewSimpleClientset(testPods...)
cmd := NewGetCmd(fakeClient)
// Test command execution
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetArgs([]string{"pods"})
err := cmd.Execute()
assert.NoError(t, err)
assert.Contains(t, buf.String(), "test-pod")
}Best Practice Recommendations:
For Unit Tests:
// Test individual components with mocked dependencies
func TestModelBoosterService_ListBoosters(t *testing.T) {
mockClient := &MockK8sClient{}
service := NewModelBoosterService(mockClient)
// Test service logic in isolation
result, err := service.ListBoosters("default")
// assertions...
}For Integration Tests:
// Test command execution with fake client
func TestGetModelBoostersCommand_Integration(t *testing.T) {
fakeClient := fake.NewClientset(testBoosters...)
service := NewModelBoosterService(fakeClient)
cmd := NewGetModelBoostersCommand(service)
// Test full command flow
output, err := cmd.Run([]string{})
// assertions...
}Migration Strategy (If You Can't Refactor Everything):
- Start new code with proper DI
- Wrap old code in testable interfaces
- Gradually refactor high-priority commands
// Temporary adapter for legacy code
type LegacyCommandAdapter struct {
getClientFunc func() (versioned.Interface, error)
}
func NewLegacyCommandAdapter(clientFunc func() (versioned.Interface, error)) *LegacyCommandAdapter {
return &LegacyCommandAdapter{getClientFunc: clientFunc}
}
func (a *LegacyCommandAdapter) Run() error {
// Calls your existing global function
client, err := a.getClientFunc()
// Run legacy logic...
}
// Test with mock
func TestLegacyCommand(t *testing.T) {
fakeClient := fake.NewClientset()
adapter := NewLegacyCommandAdapter(func() (versioned.Interface, error) {
return fakeClient, nil
})
// Test adapter...
}Key Principles:
- Explicit over implicit - Dependencies should be visible
- Interfaces over concretions - Program to interfaces
- Composition over inheritance - Build with small, testable parts
- Single Responsibility - Each component does one thing
Your current approach works, but moving to explicit dependency injection will make your code:
- ✅ More testable
- ✅ More maintainable
- ✅ More readable
- ✅ More flexible
- ✅ Less bug-prone
Start with Pattern 1 (Constructor Injection) for new code - it's the simplest and most effective for CLI tools.
|
/lgtm @git-malu any other comment |

What type of PR is this?
/kind enhancement
What this PR does / why we need it:
This pull request adds comprehensive unit tests for the CLI command package and introduces minor refactoring to improve testability and code quality. The main changes include the addition of new test files for core command functionalities, enhancements to support mocking and test data, and minor improvements to error handling and initialization logic.
Testing Improvements:
create,describe, and root commands, covering a wide range of scenarios including error cases, flag handling, and command aliases. (cli/kthena/cmd/create_test.go,cli/kthena/cmd/describe_test.go,cli/kthena/cmd/root_test.go) [1] [2] [3]cli/kthena/cmd/common_test.go)Refactoring for Testability:
getKthenaClientlogic to use an overridable function variable (getKthenaClientFunc), enabling the injection of mock clients in tests. (cli/kthena/cmd/get.go)templatesBasePathvariable configurable, allowing tests to use embedded test data directories. (cli/kthena/cmd/templates.go) [1] [2]Minor Code Improvements:
cli/kthena/cmd/root.go)fmtin the root command implementation. (cli/kthena/cmd/root.go)These changes collectively increase the reliability of the CLI commands and make the codebase easier to maintain and extend with future features.
Which issue(s) this PR fixes:
Fixes #
Special notes for your reviewer:
Does this PR introduce a user-facing change?: