Skip to content

Unit Testing

Use fake clients and interface-based design for fast, reliable unit tests.

Why Fakes Over Mocks

Fakes are simpler to maintain than mocks. They implement the same interface and let you verify behavior through state inspection rather than call verification.


Interface-Based Design

Define interfaces for testability:

// pkg/k8s/interfaces.go
package k8s

import (
    "context"

    appsv1 "k8s.io/api/apps/v1"
)

// DeploymentClient defines operations on deployments
type DeploymentClient interface {
    List(ctx context.Context, namespace string) ([]appsv1.Deployment, error)
    Restart(ctx context.Context, namespace, name string) error
}

Fake Implementation

// pkg/k8s/fake_client.go
package k8s

import (
    "context"
    "fmt"

    appsv1 "k8s.io/api/apps/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type FakeDeploymentClient struct {
    Deployments map[string]*appsv1.Deployment
    RestartLog  []string
    ListError   error
    RestartError error
}

func NewFakeDeploymentClient() *FakeDeploymentClient {
    return &FakeDeploymentClient{
        Deployments: make(map[string]*appsv1.Deployment),
        RestartLog:  []string{},
    }
}

func (f *FakeDeploymentClient) AddDeployment(namespace, name string, labels map[string]string) {
    key := fmt.Sprintf("%s/%s", namespace, name)
    f.Deployments[key] = &appsv1.Deployment{
        ObjectMeta: metav1.ObjectMeta{
            Name:      name,
            Namespace: namespace,
            Labels:    labels,
        },
    }
}

func (f *FakeDeploymentClient) List(ctx context.Context, namespace string) ([]appsv1.Deployment, error) {
    if f.ListError != nil {
        return nil, f.ListError
    }
    // Return matching deployments
    var result []appsv1.Deployment
    for _, d := range f.Deployments {
        if d.Namespace == namespace {
            result = append(result, *d)
        }
    }
    return result, nil
}

func (f *FakeDeploymentClient) Restart(ctx context.Context, namespace, name string) error {
    if f.RestartError != nil {
        return f.RestartError
    }
    f.RestartLog = append(f.RestartLog, fmt.Sprintf("%s/%s", namespace, name))
    return nil
}

Table-Driven Tests

func TestSelectDeployments(t *testing.T) {
    tests := []struct {
        name      string
        setup     func(*k8s.FakeDeploymentClient)
        wantCount int
        wantErr   bool
    }{
        {
            name: "selects matching deployments",
            setup: func(fc *k8s.FakeDeploymentClient) {
                fc.AddDeployment("default", "app-1", map[string]string{"app": "myapp"})
                fc.AddDeployment("default", "app-2", map[string]string{"app": "myapp"})
            },
            wantCount: 2,
        },
        {
            name: "returns empty for no matches",
            setup: func(fc *k8s.FakeDeploymentClient) {
                fc.AddDeployment("other", "app-1", map[string]string{"app": "myapp"})
            },
            wantCount: 0,
        },
        {
            name: "handles list error",
            setup: func(fc *k8s.FakeDeploymentClient) {
                fc.ListError = fmt.Errorf("connection refused")
            },
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            fc := k8s.NewFakeDeploymentClient()
            tt.setup(fc)

            got, err := selectDeployments(context.Background(), fc, "default")

            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
            }
            if len(got) != tt.wantCount {
                t.Errorf("got %d deployments, want %d", len(got), tt.wantCount)
            }
        })
    }
}

Testing Commands

package cmd

import (
    "bytes"
    "strings"
    "testing"

    "github.com/spf13/cobra"
)

func executeCommand(root *cobra.Command, args ...string) (string, error) {
    buf := new(bytes.Buffer)
    root.SetOut(buf)
    root.SetErr(buf)
    root.SetArgs(args)
    err := root.Execute()
    return buf.String(), err
}

func TestCheckCommand(t *testing.T) {
    output, err := executeCommand(rootCmd, "check", "--json")
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    if !strings.Contains(output, `"valid"`) {
        t.Errorf("expected JSON output with valid field")
    }
}

func TestVersionCommand(t *testing.T) {
    output, err := executeCommand(rootCmd, "version")
    if err != nil {
        t.Errorf("unexpected error: %v", err)
    }
    if !strings.Contains(output, "Version:") {
        t.Errorf("expected version output, got: %s", output)
    }
}

Parallel Tests

func TestDeploymentOperations(t *testing.T) {
    t.Run("list", func(t *testing.T) {
        t.Parallel()
        // test list operation
    })

    t.Run("restart", func(t *testing.T) {
        t.Parallel()
        // test restart operation
    })
}

Fast unit tests with fakes catch logic bugs early.

Comments