test

package
v0.0.0-...-ac495b1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 26, 2025 License: Apache-2.0 Imports: 16 Imported by: 0

README

Codex Go Test Infrastructure

Comprehensive test utilities and mock generation setup for the Codex Go rewrite.

Overview

This directory contains:

  • testhelpers.go - Reusable test helper functions
  • mocks.go - Mock generation instructions and utilities
  • testdata/ - Test fixtures and golden files
  • mocks/ - Auto-generated mock implementations
  • testhelpers_example_test.go - Usage examples

Quick Start

Running Tests
# Run all tests
make test

# Run tests with verbose output
make test-verbose

# Run only fast unit tests
make test-unit

# Run with race detector
make test-race

# Generate coverage report
make test-coverage
Generating Mocks
# Generate all mocks
make generate-mocks

# Clean and regenerate mocks
make regen-mocks

# Clean only
make clean-mocks
Golden Files
# Update all golden files
make golden-update

# Or use go test directly
go test -update ./...

Test Helpers

HTTP Mock Server

Create mock HTTP servers for testing API clients:

// Basic mock server
mockServer := test.NewHTTPMockServer(t, func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("response"))
})

// JSON mock server
response := map[string]string{"status": "ok"}
mockServer := test.NewJSONMockServer(t, http.StatusOK, response)

// Verify requests
mockServer.AssertRequestCount(t, 1)
mockServer.AssertRequestMethod(t, 0, "GET")
mockServer.AssertRequestPath(t, 0, "/api/v1/endpoint")
body := mockServer.GetRequestBody(t, 0)
Filesystem Mocks

Use in-memory or temporary filesystems for testing:

// In-memory filesystem (fast, no cleanup needed)
fs := test.NewMemFS(t)
test.WriteFileFS(t, fs, "/path/to/file.txt", []byte("content"))
test.AssertFileExistsFS(t, fs, "/path/to/file.txt")
data := test.ReadFileFS(t, fs, "/path/to/file.txt")

// Real OS filesystem (with automatic cleanup)
fs, tmpDir := test.NewOsFS(t)
test.WriteFileFS(t, fs, "file.txt", []byte("content"))
Context Helpers

Create contexts with automatic cleanup:

// Context with timeout
ctx := test.ContextWithTimeout(t, 5*time.Second)

// Context with deadline
ctx := test.ContextWithDeadline(t, time.Now().Add(5*time.Second))

// Cancellable context
ctx, cancel := test.ContextWithCancel(t)
defer cancel()

// Short context (100ms) for fast tests
ctx := test.ShortContext(t)

// Long context (5s) for integration tests
ctx := test.LongContext(t)
Async Operation Assertions

Wait for async operations to complete:

// Wait for condition to be true
test.Eventually(t, func() bool {
    return someCondition == true
}, 1*time.Second, "condition should become true")

// Wait for channel to receive value
result := test.WaitForChannel(t, ch, 1*time.Second)

// Wait for specific value
value := test.AssertChannelReceives(t, ch, 1*time.Second, "should receive value")

// Assert channel is empty
test.AssertChannelEmpty(t, ch, "channel should be empty")
Fixture Loading

Load test data from fixtures:

// Load raw fixture
data := test.LoadFixture(t, "example.json")

// Load and unmarshal JSON fixture
var config Config
test.LoadFixtureJSON(t, "config_minimal.json", &config)

Available fixtures:

  • config_minimal.json - Minimal configuration
  • config_full.json - Full configuration with all options
  • op_initialize.json - Initialize operation
  • op_send_message.json - Send message operation
  • event_message_received.json - Message received event
  • event_stream_start.json - Stream start event
  • event_stream_chunk.json - Stream chunk event
  • conversation_simple.json - Simple conversation history
  • conversation_with_tools.json - Conversation with tool calls
  • error_invalid_json.json - Invalid JSON error
  • error_not_found.json - Not found error
Golden File Testing

Compare outputs with golden files:

// JSON golden files
result := map[string]interface{}{"status": "success"}
test.GoldenJSON(t, "test_name", result)

// Text golden files
output := "Hello, World!"
test.GoldenText(t, "test_name", output)

// Update golden files when output changes
// go test -update ./...
Command Mocking

Mock command executions:

mocker := test.NewCommandMocker()

// Mock successful command
mocker.MockSuccess("git", "commit abc123")

// Mock failed command
mocker.MockError("docker", "error: daemon not running", 1)

// Get mock
cmd, ok := mocker.Get("git")
if ok {
    fmt.Println(cmd.Stdout) // "commit abc123"
    fmt.Println(cmd.ExitCode) // 0
}
Additional Helpers
// Create temporary directory (auto cleanup)
dir := test.TempDir(t)

// Write file to temp directory
path := test.WriteFile(t, dir, "test.txt", []byte("content"))

// Enhanced assertions
test.AssertNoError(t, err, "operation name")
test.AssertError(t, err, "operation name")
test.AssertErrorContains(t, err, "substring", "operation name")

// JSON helpers
data := test.MustMarshalJSON(t, obj)
test.MustUnmarshalJSON(t, data, &dest)

// Test control
test.RunParallel(t) // Mark test as parallel
test.SkipInShort(t, "reason") // Skip when -short flag
test.SkipInCI(t, "reason") // Skip in CI environment

Mock Generation

Overview

Mocks are generated using go.uber.org/mock (formerly gomock).

Setting Up Mocks
  1. Define an interface in your production code:
package mypackage

//go:generate mockgen -destination=../test/mocks/mock_repository.go -package=mocks . Repository

type Repository interface {
    Get(ctx context.Context, id string) (*Entity, error)
    Save(ctx context.Context, entity *Entity) error
    Delete(ctx context.Context, id string) error
}
  1. Generate mocks:
make generate-mocks
  1. Use mocks in tests:
package mypackage_test

import (
    "testing"
    "github.com/evmts/codex/codex-go/test/mocks"
    "go.uber.org/mock/gomock"
)

func TestMyFunction(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    mockRepo := mocks.NewMockRepository(ctrl)
    mockRepo.EXPECT().
        Get(gomock.Any(), "123").
        Return(&Entity{ID: "123"}, nil)

    // Use mockRepo in your test
    result, err := MyFunction(mockRepo)
    require.NoError(t, err)
    assert.Equal(t, "123", result.ID)
}
Mock Patterns

Basic expectation:

mock.EXPECT().Method(arg1, arg2).Return(result, nil)

Any arguments:

mock.EXPECT().Method(gomock.Any(), gomock.Any()).Return(result, nil)

Multiple calls:

mock.EXPECT().Method(arg).Return(result).Times(3)

Ordered calls:

gomock.InOrder(
    mock.EXPECT().Method1().Return(result1),
    mock.EXPECT().Method2().Return(result2),
)

Call count constraints:

mock.EXPECT().Method(arg).Return(result).MinTimes(1)
mock.EXPECT().Method(arg).Return(result).MaxTimes(5)

Custom matchers:

mock.EXPECT().Method(gomock.Eq(expected)).Return(result)
mock.EXPECT().Method(gomock.Not(gomock.Nil())).Return(result)
mock.EXPECT().Method(test.StringContains("substring")).Return(result)

Custom actions:

mock.EXPECT().Method(gomock.Any()).Do(func(arg string) {
    // Custom validation or side effects
}).Return(result)

Directory Structure

test/
├── README.md                           # This file
├── testhelpers.go                      # Test helper functions
├── testhelpers_example_test.go         # Usage examples
├── mocks.go                            # Mock utilities and docs
├── mocks/                              # Generated mocks
│   ├── README.md                       # Mocks directory documentation
│   └── mock_*.go                       # Generated mock files (auto-generated)
└── testdata/                           # Test data
    ├── README.md                       # Test data documentation
    ├── fixtures/                       # Reusable test input data
    │   ├── config_minimal.json
    │   ├── config_full.json
    │   ├── op_initialize.json
    │   ├── op_send_message.json
    │   ├── event_*.json
    │   ├── conversation_*.json
    │   └── error_*.json
    ├── golden/                         # Expected output files
    │   └── *.json / *.txt              # Golden files (test-generated)
    └── protocol/                       # Protocol test fixtures
        ├── raw_initialize.json
        ├── raw_notification.json
        ├── raw_request.json
        └── raw_response.json

Best Practices

Test Organization
  1. Use table-driven tests for multiple scenarios
  2. Run tests in parallel when possible with test.RunParallel(t)
  3. Use subtests for better organization with t.Run()
  4. Keep tests focused - one test, one concept
Mocking
  1. Mock at boundaries - HTTP clients, file system, external services
  2. Use interfaces - design for testability
  3. Avoid over-mocking - test real code when possible
  4. Keep mocks simple - complex mocks indicate design issues
Fixtures
  1. Keep fixtures minimal - only include necessary data
  2. Use realistic data - but anonymize sensitive information
  3. Version control fixtures - they're part of your tests
  4. Document complex fixtures - add .md files when needed
Golden Files
  1. Review changes carefully - golden file updates affect test behavior
  2. Commit golden files - they define expected behavior
  3. Use for complex outputs - JSON, HTML, formatted text
  4. Update intentionally - don't blindly accept changes

Dependencies

Required test dependencies (already in go.mod):

  • github.com/stretchr/testify - Assertions and test utilities
  • go.uber.org/mock - Mock generation (mockgen)
  • github.com/spf13/afero - Filesystem abstraction for testing

Installation

Install development tools:

make install-tools

This installs:

  • golangci-lint - Linter
  • mockgen - Mock generator
  • goimports - Import formatter

Examples

See testhelpers_example_test.go for comprehensive examples of all test helpers.

Run examples:

go test -v ./test/... -run=Test

Contributing

When adding new test helpers:

  1. Add the helper to testhelpers.go
  2. Add usage example to testhelpers_example_test.go
  3. Document in this README
  4. Ensure helpers use t.Helper() for better error messages

Troubleshooting

Mocks not generating?

  • Check go:generate directive syntax
  • Run make generate-mocks explicitly
  • Verify mockgen is installed: which mockgen

Tests failing in CI but passing locally?

  • Check for timing issues - use generous timeouts
  • Verify test isolation - avoid shared state
  • Use test.SkipInCI(t, "reason") for environment-specific tests

Golden files showing differences?

  • Review changes: git diff test/testdata/golden/
  • Update if intentional: make golden-update
  • Commit updated golden files

Support

For issues or questions:

  1. Check existing tests in testhelpers_example_test.go
  2. Review this documentation
  3. Check the original Rust implementation for reference
  4. Consult the team

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func AssertChannelEmpty

func AssertChannelEmpty[T any](t *testing.T, ch <-chan T, message string)

AssertChannelEmpty asserts that a channel is empty (non-blocking check)

func AssertChannelReceives

func AssertChannelReceives[T any](t *testing.T, ch <-chan T, timeout time.Duration, message string) T

AssertChannelReceives asserts that a channel receives a value within timeout

func AssertError

func AssertError(t *testing.T, err error, context string)

AssertError asserts that an error occurred

func AssertErrorContains

func AssertErrorContains(t *testing.T, err error, substring, context string)

AssertErrorContains asserts that an error contains a specific message

func AssertFileExistsFS

func AssertFileExistsFS(t *testing.T, fs afero.Fs, path string)

AssertFileExistsFS asserts that a file exists in the filesystem

func AssertFileNotExistsFS

func AssertFileNotExistsFS(t *testing.T, fs afero.Fs, path string)

AssertFileNotExistsFS asserts that a file does not exist in the filesystem

func AssertNoError

func AssertNoError(t *testing.T, err error, context string)

AssertNoError is a convenience wrapper for require.NoError with better error messages

func CaptureExecCommand

func CaptureExecCommand(t *testing.T, mocker *CommandMocker) func()

CaptureExecCommand captures exec.Command calls for testing Usage in tests:

defer CaptureExecCommand(t, mocker)()
result := yourFunctionThatCallsExecCommand()

func ContextWithCancel

func ContextWithCancel(t *testing.T) (context.Context, context.CancelFunc)

ContextWithCancel creates a cancellable context with automatic cleanup

func ContextWithDeadline

func ContextWithDeadline(t *testing.T, deadline time.Time) context.Context

ContextWithDeadline creates a context with a deadline and automatic cleanup

func ContextWithTimeout

func ContextWithTimeout(t *testing.T, timeout time.Duration) context.Context

ContextWithTimeout creates a context with a timeout and automatic cleanup

func Eventually

func Eventually(t *testing.T, condition func() bool, timeout time.Duration, message string)

Eventually repeatedly checks a condition until it passes or times out

func EventuallyWithContext

func EventuallyWithContext(t *testing.T, ctx context.Context, condition func() bool, message string)

EventuallyWithContext repeatedly checks a condition with context until it passes or times out

func FileExistsFS

func FileExistsFS(t *testing.T, fs afero.Fs, path string) bool

FileExistsFS checks if a file exists in an afero filesystem

func GoldenJSON

func GoldenJSON(t *testing.T, name string, got interface{})

GoldenJSON compares JSON output with a golden file or updates it if -update flag is set

func GoldenText

func GoldenText(t *testing.T, name string, got string)

GoldenText compares text output with a golden file or updates it if -update flag is set

func LoadFixture

func LoadFixture(t *testing.T, name string) []byte

LoadFixture loads a test fixture file from testdata/fixtures/

func LoadFixtureJSON

func LoadFixtureJSON(t *testing.T, name string, dest interface{})

LoadFixtureJSON loads and unmarshals a JSON fixture

func LongContext

func LongContext(t *testing.T) context.Context

LongContext creates a context with a 5 second timeout for longer tests

func MatchJSON

func MatchJSON(expected string) gomock.Matcher

MatchJSON creates a matcher that compares JSON strings semantically

func MustMarshalJSON

func MustMarshalJSON(t *testing.T, v interface{}) []byte

MustMarshalJSON marshals a value to JSON or fails the test

func MustUnmarshalJSON

func MustUnmarshalJSON(t *testing.T, data []byte, dest interface{})

MustUnmarshalJSON unmarshals JSON or fails the test

func NewMemFS

func NewMemFS(t *testing.T) afero.Fs

NewMemFS creates an in-memory filesystem for testing

func NewOsFS

func NewOsFS(t *testing.T) (afero.Fs, string)

NewOsFS creates a real OS filesystem with a temp directory

func ReadFileFS

func ReadFileFS(t *testing.T, fs afero.Fs, path string) []byte

ReadFileFS reads a file from an afero filesystem

func RunParallel

func RunParallel(t *testing.T)

RunParallel marks a test to run in parallel and returns a cleanup function

func ShortContext

func ShortContext(t *testing.T) context.Context

ShortContext creates a context with a short 100ms timeout for fast tests

func SkipInCI

func SkipInCI(t *testing.T, reason string)

SkipInCI skips a test when running in CI environment

func SkipInShort

func SkipInShort(t *testing.T, reason string)

SkipInShort skips a test when running with -short flag

func StringContains

func StringContains(substring string) gomock.Matcher

StringContains creates a matcher that matches strings containing the substring

func TempDir

func TempDir(t *testing.T) string

TempDir creates a temporary directory for testing

func WaitForChannel

func WaitForChannel[T any](t *testing.T, ch <-chan T, timeout time.Duration) T

WaitForChannel waits for a channel to receive a value or times out

func WriteFile

func WriteFile(t *testing.T, dir, filename string, data []byte) string

WriteFile writes data to a file in a test temporary directory

func WriteFileFS

func WriteFileFS(t *testing.T, fs afero.Fs, path string, data []byte)

WriteFileFS writes a file to an afero filesystem

Types

type CommandMocker

type CommandMocker struct {
	Commands map[string]*MockCommand
}

CommandMocker provides a way to mock command executions

func NewCommandMocker

func NewCommandMocker() *CommandMocker

NewCommandMocker creates a new command mocker

func (*CommandMocker) Get

func (m *CommandMocker) Get(cmdName string) (*MockCommand, bool)

Get retrieves a mock command by name

func (*CommandMocker) Mock

func (m *CommandMocker) Mock(cmdName string, stdout, stderr string, exitCode int)

Mock registers a mock response for a command

func (*CommandMocker) MockError

func (m *CommandMocker) MockError(cmdName, stderr string, exitCode int)

MockError registers a failed command execution

func (*CommandMocker) MockSuccess

func (m *CommandMocker) MockSuccess(cmdName, stdout string)

MockSuccess registers a successful command execution

type HTTPMockServer

type HTTPMockServer struct {
	*httptest.Server

	Requests []*http.Request // Record all requests
	Bodies   [][]byte        // Record all request bodies
	// contains filtered or unexported fields
}

HTTPMockServer creates a test HTTP server with custom handlers

func NewHTTPMockServer

func NewHTTPMockServer(t *testing.T, handler http.HandlerFunc) *HTTPMockServer

NewHTTPMockServer creates a new mock HTTP server with the given handler

func NewJSONMockServer

func NewJSONMockServer(t *testing.T, statusCode int, response interface{}) *HTTPMockServer

NewJSONMockServer creates a mock server that responds with JSON

func (*HTTPMockServer) AssertRequestCount

func (m *HTTPMockServer) AssertRequestCount(t *testing.T, expected int)

AssertRequestCount asserts the number of requests received

func (*HTTPMockServer) AssertRequestMethod

func (m *HTTPMockServer) AssertRequestMethod(t *testing.T, index int, method string)

AssertRequestMethod asserts the HTTP method of a specific request

func (*HTTPMockServer) AssertRequestPath

func (m *HTTPMockServer) AssertRequestPath(t *testing.T, index int, path string)

AssertRequestPath asserts the path of a specific request

func (*HTTPMockServer) GetRequestBody

func (m *HTTPMockServer) GetRequestBody(t *testing.T, index int) []byte

GetRequestBody returns the body of a specific request

type JSONMatcher

type JSONMatcher struct {
	// contains filtered or unexported fields
}

JSONMatcher matches JSON strings with flexible comparison

func (*JSONMatcher) Matches

func (m *JSONMatcher) Matches(x interface{}) bool

func (*JSONMatcher) String

func (m *JSONMatcher) String() string

type MockCommand

type MockCommand struct {
	Stdout   string
	Stderr   string
	ExitCode int
}

MockCommand represents a mock command execution

type MockController

type MockController struct {
	*gomock.Controller
}

MockController wraps gomock.Controller with automatic cleanup

func NewMockController

func NewMockController(t *testing.T) *MockController

NewMockController creates a new mock controller with automatic cleanup

type StringContainsMatcher

type StringContainsMatcher struct {
	// contains filtered or unexported fields
}

StringContainsMatcher matches strings containing a substring

func (*StringContainsMatcher) Matches

func (m *StringContainsMatcher) Matches(x interface{}) bool

func (*StringContainsMatcher) String

func (m *StringContainsMatcher) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL