testutil

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Mar 2, 2026 License: MIT Imports: 6 Imported by: 0

README

TestUtil - Testing Infrastructure for gokit

The testutil package provides a comprehensive testing infrastructure for gokit components, following the same lifecycle patterns as production components. It enables easy setup, teardown, and management of test components with support for state snapshots and resets.

Features

  • TestComponent Interface: Extends component.Component with testing-specific methods (Reset, Snapshot, Restore)
  • TestManager: Lifecycle manager for coordinating multiple test components
  • Helper Functions: Convenient wrappers for common testing patterns
  • Testing.T Integration: Automatic cleanup integration with Go's testing package
  • Thread-Safe: All operations are safe for concurrent use

Quick Start

Basic Usage
package mypackage_test

import (
    "testing"
    "github.com/kbukum/gokit/testutil"
)

func TestMyFeature(t *testing.T) {
    // Option 1: Manual cleanup
    cleanup, err := testutil.Setup(myComponent)
    if err != nil {
        t.Fatal(err)
    }
    defer cleanup()
    
    // Your test code here...
}

func TestMyFeatureAutoCleanup(t *testing.T) {
    // Option 2: Automatic cleanup with testing.T
    testutil.T(t).Setup(myComponent)
    
    // Component is automatically cleaned up when test ends
    // Your test code here...
}
Managing Multiple Components
func TestIntegration(t *testing.T) {
    ctx := context.Background()
    manager := testutil.NewManager(ctx)
    
    // Add components
    manager.Add(databaseComponent)
    manager.Add(redisComponent)
    manager.Add(kafkaComponent)
    
    // Start all components
    if err := manager.StartAll(); err != nil {
        t.Fatal(err)
    }
    defer manager.Cleanup()
    
    // Your integration test here...
}
State Management
func TestWithStateReset(t *testing.T) {
    testutil.T(t).Setup(dbComponent)
    
    // Run first test case
    // ... modify database state ...
    
    // Reset to initial state
    testutil.T(t).Reset(dbComponent)
    
    // Run second test case with clean state
}

func TestWithSnapshotRestore(t *testing.T) {
    testutil.T(t).Setup(dbComponent)
    
    // Setup initial test data
    // ... populate database ...
    
    // Capture current state
    snapshot := testutil.T(t).Snapshot(dbComponent)
    
    // Run test that modifies state
    // ... modify database ...
    
    // Restore to snapshot
    testutil.T(t).Restore(dbComponent, snapshot)
    
    // State is back to the snapshot point
}

Architecture

TestComponent Interface

The TestComponent interface extends component.Component with testing-specific lifecycle methods:

type TestComponent interface {
    component.Component  // Name(), Start(), Stop(), Health()
    
    // Testing-specific methods
    Reset(ctx context.Context) error
    Snapshot(ctx context.Context) (interface{}, error)
    Restore(ctx context.Context, snapshot interface{}) error
}

This hybrid approach provides:

  • Consistency: Same lifecycle pattern as production components
  • Flexibility: Can be used as both a Component and a test helper
  • Integration: Works with existing component infrastructure (Registry, etc.)
TestManager

The Manager coordinates lifecycle operations across multiple components:

  • StartAll(): Starts all components in order
  • StopAll(): Stops all components in reverse order (LIFO)
  • ResetAll(): Resets all components to initial state
  • Get(name): Retrieve a specific component by name
  • Cleanup(): Alias for StopAll() for defer usage
Helper Functions
Setup/Teardown
// Setup starts a component and returns cleanup function
cleanup, err := testutil.Setup(component)
defer cleanup()

// With custom context
cleanup, err := testutil.SetupWithContext(ctx, component)
defer cleanup()

// Teardown stops a component
err := testutil.Teardown(component)
err := testutil.TeardownWithContext(ctx, component)
Reset
// Reset a component to initial state
err := testutil.ResetComponent(component)
err := testutil.ResetComponentWithContext(ctx, component)
Testing.T Integration
// T() provides automatic cleanup integration
testutil.T(t).Setup(component)          // Auto-cleanup on test end
testutil.T(t).Reset(component)          // Reset to initial state
snapshot := testutil.T(t).Snapshot(component)    // Capture state
testutil.T(t).Restore(component, snapshot)       // Restore state

// With custom context
testutil.T(t).WithContext(ctx).Setup(component)

Best Practices

1. Use Automatic Cleanup

Prefer testutil.T(t).Setup() over manual cleanup to ensure resources are always freed:

// Good ✓
testutil.T(t).Setup(component)

// Also good ✓
cleanup, err := testutil.Setup(component)
if err != nil {
    t.Fatal(err)
}
defer cleanup()

// Avoid ✗ - easy to forget cleanup
component.Start(ctx)
// ... test code ...
component.Stop(ctx)  // might not run if test fails
2. Use Manager for Multiple Components

When testing with multiple components, use Manager to coordinate them:

manager := testutil.NewManager(ctx)
manager.Add(db)
manager.Add(redis)
manager.Add(kafka)

if err := manager.StartAll(); err != nil {
    t.Fatal(err)
}
defer manager.Cleanup()
3. Reset Between Test Cases

Use Reset() to ensure test isolation within table-driven tests:

func TestCases(t *testing.T) {
    testutil.T(t).Setup(dbComponent)
    
    tests := []struct {
        name string
        // ...
    }{
        // test cases...
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            testutil.T(t).Reset(dbComponent)  // Clean state for each case
            // ... test logic ...
        })
    }
}
4. Use Snapshots for Complex State

When you need to return to a specific state multiple times:

testutil.T(t).Setup(dbComponent)

// Setup complex test data
// ... populate database with fixtures ...

snapshot := testutil.T(t).Snapshot(dbComponent)

// Test case 1
// ... modify state ...
testutil.T(t).Restore(dbComponent, snapshot)

// Test case 2 - starts from same snapshot
// ... modify state differently ...
testutil.T(t).Restore(dbComponent, snapshot)
5. Follow LIFO Order

When manually managing cleanup, always cleanup in reverse order (LIFO):

cleanup1, _ := testutil.Setup(component1)
cleanup2, _ := testutil.Setup(component2)
cleanup3, _ := testutil.Setup(component3)

// Cleanup in reverse order
defer cleanup3()
defer cleanup2()
defer cleanup1()

// Or use Manager which handles this automatically

Creating Test Components

To create a test component for your module, implement the TestComponent interface:

package mymodule

import (
    "context"
    "github.com/kbukum/gokit/component"
    "github.com/kbukum/gokit/testutil"
)

type TestMyComponent struct {
    name string
    // ... component state ...
}

func NewTestComponent(name string) testutil.TestComponent {
    return &TestMyComponent{name: name}
}

// Component interface methods
func (c *TestMyComponent) Name() string { return c.name }

func (c *TestMyComponent) Start(ctx context.Context) error {
    // Initialize component
    return nil
}

func (c *TestMyComponent) Stop(ctx context.Context) error {
    // Cleanup resources
    return nil
}

func (c *TestMyComponent) Health(ctx context.Context) component.Health {
    return component.Health{
        Name:   c.name,
        Status: component.StatusHealthy,
    }
}

// TestComponent interface methods
func (c *TestMyComponent) Reset(ctx context.Context) error {
    // Reset to initial state
    return nil
}

func (c *TestMyComponent) Snapshot(ctx context.Context) (interface{}, error) {
    // Capture current state
    return map[string]interface{}{
        "data": c.data,
    }, nil
}

func (c *TestMyComponent) Restore(ctx context.Context, snapshot interface{}) error {
    // Restore from snapshot
    state := snapshot.(map[string]interface{})
    c.data = state["data"]
    return nil
}

Module Integration

Module-specific test utilities should be placed in a testutil subdirectory:

database/
├── testutil/
│   ├── component.go      # Database test component
│   └── memory.go         # In-memory implementation

kafka/
├── testutil/
│   ├── component.go      # Kafka test component
│   └── broker.go         # Mock broker

redis/
├── testutil/
│   ├── component.go      # Redis test component
│   └── mock.go           # Mock Redis

Each module testutil provides domain-specific test helpers while conforming to the testutil.TestComponent interface.

Examples

See the test files for comprehensive examples:

  • component_test.go - TestComponent interface tests
  • manager_test.go - TestManager usage patterns
  • helpers_test.go - Helper function examples

Thread Safety

All TestManager operations are thread-safe and can be called concurrently. Individual TestComponent implementations should also ensure thread-safety if they will be used in concurrent tests.

Next Steps

  • See module-specific testutil packages for database, Redis, Kafka, etc.
  • Review the Testing Guide for best practices (when available)
  • Check the TestUtil Cookbook for common patterns (when available)

Contributing

When adding new test components:

  1. Implement the TestComponent interface
  2. Write comprehensive tests for the component
  3. Document usage patterns in the component's README
  4. Follow the existing naming conventions and patterns

Documentation

Overview

Package testutil provides testing infrastructure for gokit components.

The testutil package extends gokit's component lifecycle pattern with testing-specific capabilities, enabling easy setup, teardown, and state management for test components.

Quick Start

Basic usage with automatic cleanup:

func TestMyFeature(t *testing.T) {
    testutil.T(t).Setup(myComponent)
    // Component is automatically cleaned up when test ends
}

Manual cleanup:

cleanup, err := testutil.Setup(myComponent)
if err != nil {
    t.Fatal(err)
}
defer cleanup()

Managing multiple components:

manager := testutil.NewManager(ctx)
manager.Add(dbComponent)
manager.Add(redisComponent)
manager.StartAll()
defer manager.Cleanup()

Architecture

The TestComponent interface extends component.Component with three testing-specific methods:

  • Reset(ctx): Restore component to initial state
  • Snapshot(ctx): Capture current state
  • Restore(ctx, snapshot): Restore to a captured state

This hybrid approach provides consistency with production code while adding testing capabilities needed for test isolation and state management.

Thread Safety

All Manager operations are thread-safe. Individual TestComponent implementations should ensure thread-safety if used in concurrent tests.

See the README.md for comprehensive documentation and examples.

Example (CustomTestComponent)

ExampleTestComponent demonstrates implementing a custom test component

package main

import (
	"fmt"

	"github.com/kbukum/gokit/component"
)

func main() {
	// This example shows the interface that test components must implement
	type MyTestComponent struct {
		name    string
		started bool
	}

	// Component interface
	_ = func(c *MyTestComponent) string { return c.name }                       // Name()
	_ = func(c *MyTestComponent) error { c.started = true; return nil }         // Start(ctx)
	_ = func(c *MyTestComponent) error { c.started = false; return nil }        // Stop(ctx)
	_ = func(c *MyTestComponent) component.Health { return component.Health{} } // Health(ctx)

	// TestComponent interface additions
	_ = func(c *MyTestComponent) error { return nil }
	_ = func(c *MyTestComponent) (interface{}, error) { return map[string]interface{}{}, nil }
	_ = func(c *MyTestComponent) error { return nil }

	fmt.Println("TestComponent interface implemented")

}
Output:

TestComponent interface implemented

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ResetComponent

func ResetComponent(component TestComponent) error

ResetComponent resets a test component to its initial state.

func ResetComponentWithContext

func ResetComponentWithContext(ctx context.Context, component TestComponent) error

ResetComponentWithContext resets a test component with a custom context.

func Teardown

func Teardown(component TestComponent) error

Teardown stops a test component. This is the inverse of Setup and is provided for symmetry.

func TeardownWithContext

func TeardownWithContext(ctx context.Context, component TestComponent) error

TeardownWithContext stops a test component with a custom context.

Types

type CleanupFunc

type CleanupFunc func() error

CleanupFunc is a function that performs cleanup, typically stopping a component.

func Setup

func Setup(component TestComponent) (CleanupFunc, error)

Setup starts a test component and returns a cleanup function. The cleanup function should be called (typically with defer) to stop the component.

Example:

cleanup, err := testutil.Setup(dbComponent)
if err != nil {
    t.Fatal(err)
}
defer cleanup()
Example

ExampleSetup demonstrates basic component setup and teardown

// Create a test component
comp := newMockComponent("my-service")

// Setup starts the component and returns cleanup function
cleanup, err := testutil.Setup(comp)
if err != nil {
	panic(err)
}
defer cleanup()

// Use the component in your test
health := comp.Health(context.Background())
fmt.Println(health.Status)
Output:

healthy

func SetupWithContext

func SetupWithContext(ctx context.Context, component TestComponent) (CleanupFunc, error)

SetupWithContext starts a test component with a custom context and returns a cleanup function.

type Manager

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

Manager provides lifecycle management for multiple test components. It allows starting, stopping, and resetting multiple components together, making it easier to manage complex test setups.

Example

ExampleManager demonstrates managing multiple components

ctx := context.Background()

// Create manager
manager := testutil.NewManager(ctx)

// Add multiple components
manager.Add(newMockComponent("database"))
manager.Add(newMockComponent("redis"))
manager.Add(newMockComponent("kafka"))

// Start all components
if err := manager.StartAll(); err != nil {
	panic(err)
}
defer manager.Cleanup()

// Get specific component
db := manager.Get("database")
fmt.Println(db.Name())
Output:

database
Example (ResetAll)

ExampleManager_resetAll demonstrates resetting all components

ctx := context.Background()
manager := testutil.NewManager(ctx)

comp := newMockComponent("test-db")
manager.Add(comp)

// Start component
manager.StartAll()

// Use component...
// (modify state)

// Reset all components to initial state
if err := manager.ResetAll(); err != nil {
	panic(err)
}

fmt.Println("Components reset successfully")
Output:

Components reset successfully

func NewManager

func NewManager(ctx context.Context) *Manager

NewManager creates a new test component manager.

func (*Manager) Add

func (m *Manager) Add(component TestComponent)

Add registers a test component with the manager.

func (*Manager) Cleanup

func (m *Manager) Cleanup() error

Cleanup is an alias for StopAll, provided for convenience. This makes it easy to use with defer or testing.T.Cleanup().

func (*Manager) Components

func (m *Manager) Components() []TestComponent

Components returns all registered components.

func (*Manager) Get

func (m *Manager) Get(name string) TestComponent

Get retrieves a component by name. Returns nil if no component with the given name is found.

func (*Manager) ResetAll

func (m *Manager) ResetAll() error

ResetAll resets all registered components to their initial state. If any component fails to reset, returns immediately with that error.

func (*Manager) StartAll

func (m *Manager) StartAll() error

StartAll starts all registered components in order. If any component fails to start, returns immediately with that error.

func (*Manager) StopAll

func (m *Manager) StopAll() error

StopAll stops all registered components in reverse order. Even if some components fail to stop, continues stopping others and returns a combined error with all failures.

type THelper

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

THelper provides testing.T integration for easier test setup.

Example (Snapshot)

ExampleTHelper_snapshot demonstrates state snapshot and restore

t := &testing.T{} // In real tests, this comes from the test function
comp := newMockComponent("stateful-service")

testutil.T(t).Setup(comp)

// Capture current state
snapshot := testutil.T(t).Snapshot(comp)

// ... modify component state ...

// Restore to previous state
testutil.T(t).Restore(comp, snapshot)

fmt.Println("State restored successfully")
Output:

State restored successfully

func T

func T(t *testing.T) *THelper

T wraps a testing.T to provide helper methods. This integrates testutil with Go's testing package for automatic cleanup.

Example:

func TestMyFeature(t *testing.T) {
    testutil.T(t).Setup(dbComponent)
    // component is automatically cleaned up when test ends
}
Example

ExampleT demonstrates automatic cleanup with testing.T

t := &testing.T{} // In real tests, this comes from the test function

// Setup with automatic cleanup
comp := newMockComponent("my-service")
testutil.T(t).Setup(comp)

// Component is started and will be automatically cleaned up when test ends
health := comp.Health(context.Background())
fmt.Println(health.Status)
Output:

healthy

func (*THelper) Reset

func (h *THelper) Reset(component TestComponent)

Reset resets a component to its initial state.

func (*THelper) Restore

func (h *THelper) Restore(component TestComponent, snapshot interface{})

Restore restores a component to a previously captured state.

func (*THelper) Setup

func (h *THelper) Setup(component TestComponent)

Setup starts a component and registers cleanup with testing.T. The component will be automatically stopped when the test ends.

func (*THelper) Snapshot

func (h *THelper) Snapshot(component TestComponent) interface{}

Snapshot captures the current state of a component.

func (*THelper) WithContext

func (h *THelper) WithContext(ctx context.Context) *THelper

WithContext sets a custom context for the helper.

type TestComponent

type TestComponent interface {
	component.Component

	// Reset restores the component to its initial state.
	// This is typically used between test cases to ensure test isolation.
	Reset(ctx context.Context) error

	// Snapshot captures the current state of the component.
	// The returned data can be passed to Restore() to return to this state.
	Snapshot(ctx context.Context) (interface{}, error)

	// Restore restores the component to a previously captured state.
	// The snapshot parameter should be a value returned by Snapshot().
	Restore(ctx context.Context, snapshot interface{}) error
}

TestComponent extends component.Component with testing-specific lifecycle methods. Test components can be used both as regular components in the component registry and as test helpers with additional Reset/Snapshot/Restore capabilities.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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