-
Notifications
You must be signed in to change notification settings - Fork 73
Description
Summary
Add unit tests for the CLI commands in cmd/cartesi-rollups-cli/. These tests should verify that the command layer works correctly: flags are parsed, environment variables are bound via Viper, subcommands are properly registered, help text is displayed, and validation errors are surfaced appropriately.
Note: This is NOT about testing business logic (already covered in library tests) or integration testing. This is purely about testing the CLI commands as units.
Motivation
The CLI commands act as a thin shell around the underlying libraries. While the libraries have their own tests, the command layer itself lacks test coverage. This creates risk for:
- Flag parsing regressions
- Environment variable binding issues (Viper)
- Subcommand registration problems
- Help text/usage display bugs
- Missing or incorrect validation at the command level
- Executing the wrong action
Scope
In Scope
- Flag parsing and default values
- Environment variable binding via Viper (using
viper.Reset()between tests) - Subcommand registration and hierarchy
- Help/usage text output
- Error messages for missing required flags or invalid values
- Version flag output
- Correct resulting action
Out of Scope
- Business logic (tested in libraries)
- Integration tests with real databases/blockchain
- E2E tests
Proposed Approach
1. Test Helper Package
Create a reusable test helper at cmd/cartesi-rollups-cli/testutil/execute.go:
package testutil
import (
"bytes"
"os"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// ExecuteResult captures the result of command execution
type ExecuteResult struct {
Output string
Err error
}
// ExecuteCommand executes a cobra command with the given args and returns the output
func ExecuteCommand(t *testing.T, cmd *cobra.Command, args ...string) ExecuteResult {
t.Helper()
// Reset viper state to avoid pollution between tests
viper.Reset()
buf := new(bytes.Buffer)
cmd.SetOut(buf)
cmd.SetErr(buf)
cmd.SetArgs(args)
err := cmd.Execute()
return ExecuteResult{
Output: buf.String(),
Err: err,
}
}
// WithEnvVars sets environment variables for the duration of a test
func WithEnvVars(t *testing.T, vars map[string]string) func() {
t.Helper()
original := make(map[string]string)
for k, v := range vars {
original[k] = os.Getenv(k)
os.Setenv(k, v)
}
return func() {
for k := range vars {
if orig, ok := original[k]; ok && orig != "" {
os.Setenv(k, orig)
} else {
os.Unsetenv(k)
}
}
}
}2. Test Structure
Each command package should have a corresponding _test.go file:
cmd/cartesi-rollups-cli/
├── main.go
├── main_test.go # Tests for main binary behavior
├── testutil/
│ └── execute.go # Shared test helpers
└── root/
├── root.go
├── root_test.go # Tests for root command
├── send/
│ ├── send.go
│ └── send_test.go # Tests for send command
├── read/
│ ├── read.go
│ └── read_test.go
├── inspect/
│ ├── inspect.go
│ └── inspect_test.go
├── validate/
│ ├── validate.go
│ └── validate_test.go
├── execute/
│ ├── execute.go
│ └── execute_test.go
├── app/
│ ├── app.go
│ └── app_test.go
├── db/
│ ├── db.go
│ └── db_test.go
└── deploy/
├── deploy.go
└── deploy_test.go
3. Test Categories
3.1 Root Command Tests (root/root_test.go)
package root_test
import (
"testing"
"github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/root"
"github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRootCommand_Help(t *testing.T) {
result := testutil.ExecuteCommand(t, root.Cmd, "--help")
require.NoError(t, result.Err)
assert.Contains(t, result.Output, "Command line interface for the Cartesi Rollups Node")
assert.Contains(t, result.Output, "Available Commands:")
}
func TestRootCommand_Version(t *testing.T) {
result := testutil.ExecuteCommand(t, root.Cmd, "--version")
require.NoError(t, result.Err)
assert.Contains(t, result.Output, "cartesi-rollups-cli")
}
func TestRootCommand_SubcommandsRegistered(t *testing.T) {
result := testutil.ExecuteCommand(t, root.Cmd, "--help")
require.NoError(t, result.Err)
expectedSubcommands := []string{"send", "read", "inspect", "validate", "execute", "app", "db", "deploy"}
for _, subcmd := range expectedSubcommands {
assert.Contains(t, result.Output, subcmd, "subcommand %s should be registered", subcmd)
}
}
func TestRootCommand_UnknownSubcommand(t *testing.T) {
result := testutil.ExecuteCommand(t, root.Cmd, "unknown-command")
assert.Error(t, result.Err)
assert.Contains(t, result.Output, "unknown command")
}3.2 Flag Parsing Tests
func TestRootCommand_DatabaseConnectionFlag(t *testing.T) {
tests := []struct {
name string
args []string
wantErr bool
}{
{
name: "valid connection string",
args: []string{"--database-connection", "postgres://user:pass@localhost:5432/db"},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Note: This may require a subcommand that actually uses the flag
// or testing via viper.GetString() after execution
result := testutil.ExecuteCommand(t, root.Cmd, tt.args...)
if tt.wantErr {
assert.Error(t, result.Err)
}
})
}
}3.3 Environment Variable Binding Tests
func TestRootCommand_EnvVarBinding(t *testing.T) {
cleanup := testutil.WithEnvVars(t, map[string]string{
"DATABASE_CONNECTION": "postgres://env:env@localhost:5432/envdb",
})
defer cleanup()
// Execute a command that triggers viper config loading
// Then verify viper.GetString(config.DATABASE_CONNECTION) returns expected value
}
func TestRootCommand_FlagOverridesEnvVar(t *testing.T) {
cleanup := testutil.WithEnvVars(t, map[string]string{
"DATABASE_CONNECTION": "postgres://env:env@localhost:5432/envdb",
})
defer cleanup()
// Execute with explicit flag, verify flag takes precedence
}3.4 Subcommand Tests (example for send)
// send/send_test.go
package send_test
import (
"testing"
"github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/root"
"github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSendCommand_Help(t *testing.T) {
result := testutil.ExecuteCommand(t, root.Cmd, "send", "--help")
require.NoError(t, result.Err)
assert.Contains(t, result.Output, "send")
}
func TestSendCommand_MissingRequiredFlags(t *testing.T) {
result := testutil.ExecuteCommand(t, root.Cmd, "send")
// Verify appropriate error for missing required flags
// (adjust based on actual send command requirements)
}
func TestSendCommand_SubcommandsRegistered(t *testing.T) {
result := testutil.ExecuteCommand(t, root.Cmd, "send", "--help")
require.NoError(t, result.Err)
// Assert expected send subcommands are present
}4. Important Considerations
Viper State Isolation
The global Viper instance maintains state between tests. Always call viper.Reset() at the start of each test to ensure isolation:
func TestSomething(t *testing.T) {
viper.Reset() // Critical for test isolation
// ... rest of test
}Environment Variable Cleanup
Always restore environment variables after tests:
func TestWithEnvVar(t *testing.T) {
original := os.Getenv("MY_VAR")
defer func() {
if original != "" {
os.Setenv("MY_VAR", original)
} else {
os.Unsetenv("MY_VAR")
}
}()
os.Setenv("MY_VAR", "test-value")
// ... test code
}Table-Driven Tests
Use table-driven tests for comprehensive flag coverage:
func TestFlags(t *testing.T) {
tests := []struct {
name string
args []string
envVars map[string]string
wantErr bool
wantContain string
}{
{
name: "help flag",
args: []string{"--help"},
wantErr: false,
wantContain: "Usage:",
},
// ... more cases
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
viper.Reset()
// ... setup and assertions
})
}
}Acceptance Criteria
- Test helper package created at
cmd/cartesi-rollups-cli/testutil/ - Root command tests implemented covering:
- Help output
- Version output
- All subcommands registered
- Unknown command error
- Each subcommand has basic tests for:
- Help output
- Subcommand-specific flags
- Required flag validation (where applicable)
- Environment variable binding tests for persistent flags
- Tests pass with
go test ./cmd/cartesi-rollups-cli/... - No test pollution (tests can run in any order)
References
- [Cobra Testing Documentation](https://github.com/spf13/cobra/blob/main/command_test.go)
- [Viper Reset for Testing](https://pkg.go.dev/github.com/spf13/viper#Reset)
- [Testing Cobra CLI Commands](https://gianarb.it/blog/golang-mockmania-cli-command-with-cobra)
- [Cobra Issue #770: Testing Best Practices](Document a best practice for testing a cobra app spf13/cobra#770)
Labels
testing, cli, enhancement