Skip to content

Implement Unit Tests for cartesi-rollups-cli Commands #728

@vfusco

Description

@vfusco

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

Labels

testing, cli, enhancement

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions