| name | testing-patterns |
| description | Use when writing tests for tools, the executor, Artifact Hub client, or Bleve index. Covers unit tests with mocked executor, integration tests with build tags, e2e tests against live clusters, and table-driven patterns. |
| allowed-tools | Read, Write, Edit, Bash, Grep |
Testing Patterns
Three Test Tiers
| Tier | Build tag | Runs in CI | Needs | Location |
|---|
| Unit | (none) | always | nothing | *_test.go next to code |
| Integration | //go:build integration | on PR | docker (for k3d) | *_test.go next to code |
| E2E | //go:build e2e | nightly / manual | running k3d cluster + tilt | *_test.go next to code |
Unit Tests — Mock the Executor
package tools
import (
"context"
"testing"
"github.com/remiphilippe/mcp-devinfra/internal/executor"
)
func TestKubectlGet_BuildsCorrectArgs(t *testing.T) {
mock := executor.NewMock(executor.MockResult{
Stdout: []byte(`{"items":[]}`),
})
input := KubectlGetInput{
Resource: "pods",
Namespace: strPtr("default"),
Selector: strPtr("app=web"),
}
_, err := handleKubectlGet(context.Background(), mock, input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
mock.AssertCalledWith(t, "kubectl", "get", "pods", "-o", "json", "-n", "default", "-l", "app=web")
}
func TestKubectlGet_HandlesErrorOutput(t *testing.T) {
mock := executor.NewMock(executor.MockResult{
Stderr: []byte("error: the server doesn't have a resource type \"foos\""),
Err: fmt.Errorf("exit status 1"),
})
input := KubectlGetInput{Resource: "foos"}
_, err := handleKubectlGet(context.Background(), mock, input)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "kubectl_get") {
t.Errorf("error should be wrapped with tool name, got: %v", err)
}
}
func strPtr(s string) *string { return &s }
Table-Driven Tests
Use table-driven for tools with many input combinations:
func TestHelmInstall_ArgVariants(t *testing.T) {
tests := []struct {
name string
input HelmInstallInput
wantArgs []string
}{
{
name: "minimal",
input: HelmInstallInput{Release: "redis", Chart: "bitnami/redis"},
wantArgs: []string{"install", "redis", "bitnami/redis", "-o", "json"},
},
{
name: "with namespace and set",
input: HelmInstallInput{
Release: "redis",
Chart: "bitnami/redis",
Namespace: strPtr("cache"),
Set: []string{"auth.enabled=false"},
},
wantArgs: []string{"install", "redis", "bitnami/redis", "-o", "json",
"-n", "cache", "--set", "auth.enabled=false"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := executor.NewMock(executor.MockResult{
Stdout: []byte(`{"name":"redis","namespace":"default"}`),
})
_, err := handleHelmInstall(context.Background(), mock, tt.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
mock.AssertCalledWith(t, "helm", tt.wantArgs...)
})
}
}
Integration Tests
package tools
import (
"context"
"testing"
"time"
)
func TestKubectlGet_RealCluster(t *testing.T) {
exec := executor.New(executor.DefaultConfig())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
input := KubectlGetInput{
Resource: "namespaces",
}
output, err := handleKubectlGet(ctx, exec, input)
if err != nil {
t.Fatalf("kubectl get namespaces failed: %v", err)
}
if len(output.Resources) == 0 {
t.Fatal("expected at least one namespace (kube-system)")
}
}
Artifact Hub Client Tests
func TestArtifactHubSearch_Real(t *testing.T) {
client := artifacthub.New(artifacthub.Config{
BaseURL: "https://artifacthub.io/api/v1",
Timeout: 10 * time.Second,
})
result, err := client.Search(context.Background(), "redis", 3)
if err != nil {
t.Fatalf("search failed: %v", err)
}
if len(result.Packages) == 0 {
t.Fatal("expected results for 'redis'")
}
}
Run Commands
go test ./...
go test -tags=integration ./...
go test -tags=e2e ./...
go test -v -run TestKubectlGet ./internal/tools/
go test -race ./...
go test -count=1 ./...