Unit

A zero-dependency library for testing Ergo Framework actors with fluent API

Introduced in Ergo Framework 3.1.0

The Ergo Unit Testing Library makes testing actor-based systems simple and reliable. It provides specialized tools designed specifically for the unique challenges of testing actors, with zero external dependencies and an intuitive, readable API.

What You'll Learn

This guide takes you from simple actor tests to complex distributed scenarios. Here's the journey:

Getting Started (You Are Here!)

  • Your First Test - Simple echo and counter examples

  • Built-in Assertions - Simple tools for common checks

  • Basic Message Testing - Verify actors send the right messages

  • Basic Logging Testing - Verify your actors provide good observability

Intermediate Skills (Next Steps)

  • Configuration Testing - Test environment-driven behavior

  • Complex Message Patterns - Handle sophisticated message flows

  • Basic Process Spawning - Test actor creation and lifecycle

  • Event Inspection - Debug and analyze actor behavior

Advanced Features (When You Need Them)

  • Actor Termination - Test error handling and graceful shutdowns

  • Exit Signals - Manage process lifecycles in supervision trees

  • Scheduled Operations - Test cron jobs and time-based behavior

  • Network & Distribution - Test multi-node actor systems

Expert Level (Complex Scenarios)

  • Dynamic Value Capture - Handle generated IDs, timestamps, and random data

  • Complex Workflows - Test multi-step business processes

  • Performance & Load Testing - Verify behavior under stress

Tip: The documentation follows this learning path. You can jump to advanced topics if needed, but starting from the beginning ensures you understand the foundations.

Why Testing Actors is Different

Traditional testing tools don't work well with actors. Here's why:

The Challenge: Actors Are Not Functions

Regular code testing follows a simple pattern:

But actors are fundamentally different:

  • They run asynchronously - you send a message and the response comes later

  • They maintain state - previous messages affect future behavior

  • They spawn other actors - creating complex hierarchies

  • They communicate only via messages - no direct access to internal state

  • They can fail and restart - requiring lifecycle testing

What Makes Actor Testing Hard

  1. Asynchronous Communication

  1. Message Flow Complexity

  1. Dynamic Process Creation

  1. State Changes Over Time

How This Library Solves Actor Testing

The Ergo Unit Testing Library addresses each of these challenges:

Event Capture - See Everything Your Actor Does

Instead of guessing what happened, the library automatically captures every actor operation:

Fluent Assertions - Test What Matters

Express your test intentions clearly:

Dynamic Value Handling - Work With Generated Data

Capture and reuse dynamically generated values:

State Testing Through Behavior - Verify State Changes

Test state indirectly by verifying behavioral changes:

Why Zero Dependencies Matters

Actor testing is complex enough without dependency management headaches:

  • No version conflicts - Works with any Go testing setup

  • No external tools - Everything needed is built-in

  • Simple imports - Just import "ergo.services/ergo/testing/unit"

  • Fast execution - No overhead from external libraries

Core Concepts

Now that you understand why actor testing is different, let's explore the key concepts that make this library work.

The Event-Driven Testing Model

Everything your actor does becomes a testable "event".

When you run this simple test:

Here's what happens behind the scenes:

  1. Your actor receives the message - Normal actor behavior

  2. Your actor sends a response - Normal actor behavior

  3. The library captures a SendEvent - Testing magic

  4. You verify the captured event - Your assertion

The library automatically captures these events:

  • SendEvent - When your actor sends a message

  • SpawnEvent - When your actor creates child processes

  • LogEvent - When your actor writes log messages

  • TerminateEvent - When your actor shuts down

Why Events Matter

Events solve the fundamental challenge of testing asynchronous systems:

Instead of this (impossible):

You do this (works perfectly):

The Fluent Assertion API

The library provides a readable, chainable API that expresses test intentions clearly:

Benefits of the fluent API:

  • Readable - Tests read like English sentences

  • Discoverable - IDE autocomplete guides you through options

  • Flexible - Chain only the validations you need

  • Precise - Specify exactly what matters for each test

Installation

Your First Actor Test

Let's start with the simplest possible actor test to understand the basics:

A Simple Echo Actor

Testing the Echo Actor

What Just Happened?

This simple test demonstrates the core pattern:

  1. unit.Spawn() - Creates a test actor in an isolated environment

  2. actor.SendMessage() - Sends a message to your actor (like prod would)

  3. actor.ShouldSend() - Verifies that your actor sent the expected message

Key insight: You're not testing internal state - you're testing behavior. You verify what the actor does (sends messages) rather than what it contains (internal variables).

Why This Works

The testing library automatically captures everything your actor does:

  • Every message sent by your actor

  • Every process spawned by your actor

  • Every log message written by your actor

  • When your actor terminates

Then it provides fluent assertions to verify these captured events.

Adding Slightly More Complexity

Let's test an actor that maintains some state:

This shows how you test stateful behavior without accessing internal state - by observing how the actor's responses change over time.

Built-in Assertions

Before diving into complex actor testing, let's cover the simple assertion utilities you'll use throughout your tests.

Why Built-in Assertions Matter for Actor Testing:

Actor tests often need to verify simple conditions alongside complex event assertions. Rather than forcing you to import external testing libraries (which could conflict with your project dependencies), the unit testing library provides everything you need:

Available Assertions

Equality Testing:

Boolean Testing:

Nil Testing:

String Testing:

Type Testing:

Why Zero Dependencies Matter

No Import Conflicts:

Consistent Error Messages: All assertions provide clear, consistent error messages that integrate well with the actor testing output.

Framework Agnostic: Works with any Go testing setup - standard go test, IDE test runners, CI/CD systems, etc.

Basic Message Testing

Now that you understand the fundamentals, let's explore message testing in more depth.

What Comes Next

Now you'll learn how to test different aspects of actor behavior, building from simple to complex:

Fundamentals (You're here!)

  • Basic message sending and receiving

  • Simple process creation

  • Logging and observability

  • Configuration testing

Intermediate Skills

  • Complex message patterns

  • Event inspection and debugging

  • Actor lifecycle and termination

  • Error handling and recovery

Advanced Features

  • Scheduled operations (cron jobs)

  • Network and distribution

  • Performance and load testing

Basic Logging Testing

Logging is crucial for production actors - it provides visibility into what your actors are doing and helps with debugging. Let's learn how to test logging behavior.

Why Test Logging?

Logging tests ensure:

  • Your actors provide sufficient information for monitoring

  • Debug information is available when needed

  • Log levels are respected (don't log debug in production)

  • Sensitive operations are properly audited

Simple Logging Test

Testing Different Log Levels

Testing Log Content

Logging Best Practices for Testing

Structure your log messages to make them easy to test:

Test log levels appropriately:

  • Error - Test that errors are logged when they occur

  • Warning - Test that concerning but non-fatal events are captured

  • Info - Test that important business events are recorded

  • Debug - Test that detailed troubleshooting info is available

Intermediate Skills

Now that you've mastered the basics, let's tackle more complex testing scenarios.

Configuration and Environment Testing

Real actors often behave differently based on configuration. Let's test this:

The Spawn function creates an isolated testing environment for your actor. Unlike production actors that run in a complex node environment, test actors run in a controlled sandbox where every operation is captured for verification.

Key Benefits:

  • Isolation: Each test actor runs independently without affecting other tests

  • Deterministic: Test outcomes are predictable and repeatable

  • Observable: All actor operations are automatically captured as events

  • Configurable: Fine-tune the testing environment to match your needs

Example Actor:

Test Implementation:

Configuration Options - Fine-Tuning the Test Environment

Test configuration allows you to simulate different runtime conditions without requiring complex setup:

Environment Variables (WithEnv): Test how your actors behave with different configurations without changing production code. Useful for testing feature flags, database URLs, timeout values, and other configuration-driven behavior.

Log Levels (WithLogLevel): Control the verbosity of test output and verify that your actors log appropriately at different levels. Critical for testing monitoring and debugging capabilities.

Process Hierarchy (WithParent, WithRegister): Test actors that need to interact with parent processes or require specific naming for registration-based lookups.

Message Testing

ShouldSend() - Verifying Actor Communication

Message testing is the heart of actor validation. Since actors communicate exclusively through messages, verifying message flow is crucial for ensuring correct behavior.

Why Message Testing Matters:

  • Validates Integration: Ensures actors communicate correctly with their dependencies

  • Confirms Business Logic: Verifies that the right messages are sent in response to inputs

  • Detects Side Effects: Catches unintended message sends that could cause bugs

  • Tests Message Content: Validates that message payloads contain correct data

Example Actor:

Test Implementation:

Advanced Message Matching - Flexible Validation Patterns

When testing complex message structures or dynamic content, the library provides powerful matching capabilities:

Pattern Matching Benefits:

  • Partial Validation: Test only the fields that matter for your specific test case

  • Dynamic Content Handling: Validate messages with timestamps, UUIDs, or generated IDs

  • Type Safety: Ensure messages are of the correct type even when content varies

  • Negative Testing: Verify that certain messages are NOT sent in specific scenarios

Process Spawning

ShouldSpawn() - Testing Process Lifecycle Management

Process spawning is a fundamental actor pattern for building hierarchical systems. The testing library provides comprehensive tools for verifying that actors create, configure, and manage child processes correctly.

Why Process Spawning Tests Matter:

  • Resource Management: Ensure actors don't spawn too many or too few processes

  • Configuration Propagation: Verify that child processes receive correct configuration

  • Error Handling: Test behavior when process spawning fails

  • Supervision Trees: Validate that supervisors manage their children appropriately

Example Actor:

Test Implementation:

Dynamic Process Testing - Handling Generated Values

Real-world actors often generate dynamic values like session IDs, request tokens, or timestamps. The library provides sophisticated tools for capturing and validating these dynamic values.

Dynamic Value Testing Scenarios:

  • Session Management: Test actors that create sessions with generated IDs

  • Request Tracking: Verify that request tokens are properly generated and used

  • Time-based Operations: Validate actors that schedule work or create timestamps

  • Resource Allocation: Test dynamic assignment of resources to processes

Remote Spawn Testing

ShouldRemoteSpawn() - Testing Distributed Actor Creation

Remote spawn testing allows you to verify that actors correctly create processes on remote nodes in a distributed system. The testing library captures RemoteSpawnEvent operations and provides fluent assertions for validation.

Why Test Remote Spawning:

  • Distribution Logic: Ensure actors spawn processes on the correct remote nodes

  • Load Distribution: Verify round-robin or other distribution strategies work correctly

  • Error Handling: Test behavior when remote nodes are unavailable

  • Resource Management: Validate that remote spawning respects capacity limits

Example Actor:

Test Implementation:

Advanced Remote Spawn Patterns:

  • Multi-Node Distribution: Test round-robin or other distribution strategies across multiple nodes

  • Error Scenarios: Verify proper error handling when nodes are unavailable

  • Event Inspection: Direct inspection of RemoteSpawnEvent for detailed validation

  • Negative Assertions: Ensure remote spawns don't happen under certain conditions

Actor Termination Testing

ShouldTerminate() - Testing Actor Lifecycle Completion

Actor termination is a critical aspect of actor systems. Actors can terminate for various reasons: normal completion, explicit shutdown, or errors. The testing library provides comprehensive tools for validating termination behavior and ensuring proper cleanup.

Why Test Actor Termination:

  • Resource Cleanup: Ensure actors properly clean up resources when terminating

  • Error Propagation: Verify that errors are handled correctly and lead to appropriate termination

  • Graceful Shutdown: Test that actors respond correctly to shutdown signals

  • Supervision Trees: Validate that supervisors handle child termination appropriately

Termination Reasons:

  • gen.TerminateReasonNormal - Normal completion of actor work

  • gen.TerminateReasonShutdown - Graceful shutdown request

  • Custom errors - Abnormal termination due to specific errors

Example Actor:

Test Implementation:

Advanced Termination Patterns:

Exit Signal Testing

ShouldSendExit() - Testing Graceful Process Termination

Exit signals (SendExit and SendExitMeta) are used to gracefully terminate other processes. This is different from actor self-termination - it's about one actor telling another to exit. The testing library provides comprehensive assertions for validating exit signal behavior.

Why Test Exit Signals:

  • Graceful Shutdown: Ensure supervisors can properly terminate child processes

  • Resource Cleanup: Verify that exit signals trigger proper cleanup in target processes

  • Error Propagation: Test that failure conditions are communicated via exit signals

  • Supervision Trees: Validate that supervisors manage process lifecycles correctly

Example Actor:

Test Implementation:

Exit Signal Testing Methods

Basic Exit Signal Assertions:

Advanced Exit Signal Patterns:

Cron Testing

ShouldAddCronJob(), ShouldExecuteCronJob() - Testing Scheduled Operations

Cron job testing allows you to validate scheduled operations in your actors without waiting for real time to pass. The testing library provides comprehensive mock time support and detailed cron job lifecycle management.

Why Test Cron Jobs:

  • Schedule Validation: Ensure cron expressions are correct and jobs run at expected times

  • Job Management: Test job addition, removal, enabling, and disabling operations

  • Execution Logic: Verify that scheduled operations perform correctly when triggered

  • Time Control: Use mock time to test time-dependent behavior deterministically

Cron Testing Features:

  • Mock Time Support: Control time flow for deterministic testing

  • Job Lifecycle Testing: Validate job creation, scheduling, execution, and cleanup

  • Event Tracking: Monitor all cron-related operations and state changes

  • Schedule Simulation: Test complex scheduling scenarios without real time delays

Example Actor:

Test Implementation:

Cron Testing Methods

Job Lifecycle Assertions:

Mock Time Control:

Advanced Cron Patterns:

Built-in Assertions

The library includes a comprehensive set of zero-dependency assertion functions that cover common testing scenarios without requiring external testing frameworks:

Why Built-in Assertions:

  • Zero Dependencies: Avoid version conflicts and complex dependency management

  • Consistent Interface: All assertions follow the same pattern and error reporting

  • Testing Framework Agnostic: Works with any Go testing approach

  • Actor-Specific: Designed specifically for the needs of actor testing

Advanced Features

Dynamic Value Capture - Testing Generated Content

Real-world actors frequently generate dynamic values like timestamps, UUIDs, session IDs, or auto-incrementing counters. Traditional testing approaches struggle with these values because they're unpredictable. The library provides sophisticated capture mechanisms to handle these scenarios elegantly.

The Challenge of Dynamic Values:

  • Timestamps: Created at runtime, impossible to predict exact values

  • UUIDs: Randomly generated, different in every test run

  • Auto-incrementing IDs: Dependent on execution order and system state

  • Process IDs: Assigned by the actor system, not controllable in tests

The Solution - Value Capture:

Capture Strategies:

  • Immediate Capture: Capture values as soon as they're generated

  • Pattern Matching: Use validation functions to identify and validate dynamic content

  • Structured Matching: Validate message structure while ignoring specific dynamic fields

  • Cross-Reference Testing: Use captured values in multiple assertions to ensure consistency

Event Inspection - Deep System Analysis

For complex testing scenarios or debugging difficult issues, the library provides direct access to the complete event timeline. This allows you to perform sophisticated analysis of actor behavior beyond what's possible with standard assertions.

Events() - Complete Event History

Access all captured events for detailed analysis:

LastEvent() - Most Recent Operation

Get the most recently captured event:

ClearEvents() - Reset Event History

Clear all captured events, useful for isolating test phases:

Event Inspection Use Cases:

  • Performance Analysis: Count operations to identify performance bottlenecks

  • Workflow Validation: Ensure complex multi-step processes execute in the correct order

  • Error Investigation: Analyze the complete event sequence leading to failures

  • Integration Testing: Verify that multiple actors interact correctly in complex scenarios

Timeout Support - Assertion Timing Control

The library provides timeout support for assertions that might need time-based validation:

Timeout Function Usage:

  • Assertion Wrapping: Wrap assertion functions to add timeout behavior

  • Integration Testing: Useful when testing with external systems that might have delays

  • Performance Validation: Ensure assertions complete within expected time limits

Testing Patterns and Best Practices

Test Organization Strategies

Single Responsibility Testing: Each test should focus on one specific behavior or scenario. This makes tests easier to understand, debug, and maintain.

State Isolation: Each test should start with a clean state and not depend on other tests. Use actor.ClearEvents() when needed to reset event history between test phases.

Error Path Testing: Don't just test the happy path. Actor systems need robust error handling, so test failure scenarios thoroughly:

Message Design for Testability

Structured Messages: Design your messages to be easily testable by using structured types rather than primitive values:

Predictable vs Dynamic Content: Separate predictable content from dynamic content in your messages to make testing easier:

Performance Testing Considerations

Event Overhead: While event capture is lightweight, be aware that every operation creates events. For performance-critical tests, you can:

  • Clear events periodically with ClearEvents()

  • Focus assertions on specific time windows

  • Use event inspection to identify performance bottlenecks

Scaling Testing: Test how your actors behave under load by simulating multiple concurrent operations:

Best Practices

  1. Use descriptive test names that clearly indicate what behavior is being tested

  2. Test all message types your actor handles, including edge cases

  3. Capture dynamic values early using the Capture() method for generated IDs

  4. Test error conditions not just the happy path

  5. Use pattern matching for complex message validation

  6. Clear events between test phases when needed with ClearEvents()

  7. Configure appropriate log levels for debugging vs production testing

  8. Test temporal behaviors with timeout mechanisms

  9. Validate distributed scenarios using network simulation

  10. Organize tests by behavior rather than by implementation details

This testing library provides comprehensive coverage for all Ergo Framework actor patterns while maintaining zero external dependencies and excellent readability. By following these patterns and practices, you can build robust, well-tested actor systems that behave correctly in both simple and complex scenarios.

Complete Examples and Use Cases

The library includes comprehensive test examples organized into feature-specific files that demonstrate all capabilities through real-world scenarios:

Feature-Based Test Files

basic_test.go - Fundamental Actor Testing

  • Basic actor functionality and message handling

  • Dynamic value capture and validation

  • Built-in assertions and event tracking

  • Core testing patterns and best practices

network_test.go - Distributed System Testing

  • Remote node simulation and connectivity

  • Network configuration and route management

  • Remote spawn operations and event capture

  • Multi-node interaction patterns

workflow_test.go - Complex Business Logic

  • Multi-step order processing workflows

  • State machine validation and transitions

  • Business process orchestration

  • Error handling and recovery scenarios

call_test.go - Synchronous Communication

  • Call operations and response handling

  • Async call patterns and timeouts

  • Send/response communication flows

  • Concurrent request management

cron_test.go - Scheduled Operations

  • Cron job lifecycle management

  • Mock time control and schedule validation

  • Job execution tracking and assertions

  • Time-dependent behavior testing

termination_test.go - Actor Lifecycle Management

  • Actor termination handling and cleanup

  • Exit signal testing (SendExit/SendExitMeta)

  • Normal vs abnormal termination scenarios

  • Resource cleanup validation

Comprehensive Test Examples

  1. Complex State Machine Testing (workflow_test.go)

    • Multi-step order processing workflow

    • Validation, payment, and fulfillment pipeline

    • State transition validation and error handling

  2. Process Management (basic_test.go)

    • Dynamic worker spawning and management

    • Resource capacity limits and monitoring

    • Worker lifecycle (start, stop, restart)

  3. Advanced Pattern Matching (basic_test.go)

    • Structure matching with partial validation

    • Dynamic value handling and field validation

    • Complex conditional message matching

  4. Remote Spawn Testing (network_test.go)

    • Remote spawn operations on multiple nodes

    • Round-robin distribution testing

    • Error handling for unavailable nodes

    • Event inspection and workflow validation

  5. Cron Job Management (cron_test.go)

    • Job scheduling and execution validation

    • Mock time control for deterministic testing

    • Schedule expression testing and validation

  6. Actor Termination (termination_test.go)

    • Normal and abnormal termination scenarios

    • Exit signal testing and process cleanup

    • Termination reason validation

    • Post-termination behavior verification

  7. Concurrent Operations (call_test.go)

    • Multi-client concurrent request handling

    • Resource contention and capacity management

    • Load testing and performance validation

  8. Environment & Configuration (basic_test.go)

    • Environment variable management

    • Runtime configuration changes

    • Feature flag and conditional behavior testing

Getting Started with Examples

Learning Path

  1. Start with Basic Examples: basic_test.go - Core functionality and patterns

  2. Explore Message Testing: basic_test.go - Message flow and assertions

  3. Learn Process Management: basic_test.go - Spawn operations and lifecycle

  4. Master Synchronous Communication: call_test.go - Calls and responses

  5. Study Complex Workflows: workflow_test.go - Business logic testing

  6. Practice Network Testing: network_test.go - Distributed operations

  7. Explore Scheduling: cron_test.go - Time-based operations

  8. Understand Termination: termination_test.go - Lifecycle completion

Each test file provides complete, working implementations of specific actor patterns and demonstrates best practices for testing each scenario. All tests include comprehensive comments explaining the testing strategy and validation approach.

Configuration and Environment Testing

Real actors often behave differently based on configuration. Let's test this:

Complex Message Patterns

As your actors become more sophisticated, your message testing needs to handle more complex scenarios:

Testing Message Sequences

Testing Conditional Logic

Basic Process Spawning

Many actors need to create child processes. Here's how to test this:

Capturing Dynamic Process IDs

When actors spawn processes, you often need to use the generated PID in subsequent tests:

Event Inspection for Debugging

When tests fail, you need to understand what actually happened:

Failure Injection Testing

Overview

The Ergo Unit Testing Library includes a failure injection system that allows you to test how your actors handle various error conditions. This is essential for building robust actor systems that can gracefully handle failures in production.

Method Failure Injection

Access failure injection through the actor's Process() method:

Available Failure Methods

The failure injection system provides several methods on TestProcess:

Common Use Cases

Testing Spawn Failures

Testing Message Send Failures

Testing Intermittent Failures

Testing Pattern-Based Failures

Testing One-Time Failures

Advanced Testing Scenarios

Testing Supervisor Restart Strategies

Testing Method Call Tracking

Best Practices

  1. Clear Events Between Test Phases: Use ClearEvents() when transitioning between test phases to avoid assertion confusion.

  2. Test Recovery: Always test that your actors can recover after failures are cleared or when using one-time failures.

  3. Verify Call Counts: Use GetMethodCallCount() to ensure methods are called the expected number of times.

  4. Pattern Matching: Use pattern-based failures to test scenarios where only specific inputs should fail.

  5. Combine with Supervision: Test how supervisors handle child failures by injecting spawn failures during restart attempts.

Common Pitfalls

  1. Event Accumulation: Events accumulate across multiple operations. Use ClearEvents() to reset between test phases.

  2. Timing Issues: Some assertions may need time to complete. Use appropriate timeouts and consider async patterns.

  3. Message Ordering: In high-throughput scenarios, message ordering might not be guaranteed. Test for this explicitly.

  4. State Leakage: Each test should start with clean state. Don't rely on previous test state.

  5. Failure Persistence: Remember that SetMethodFailure persists until cleared, while SetMethodFailureOnce only fails once.

Conclusion

The Ergo Framework unit testing library provides comprehensive tools for testing actor-based systems. From simple message exchanges to complex distributed workflows, you can validate every aspect of your actor behavior with confidence.

Key Takeaways:

  • Start Simple: Begin with basic message testing and gradually add complexity

  • Test Comprehensively: Cover happy paths, error conditions, and edge cases

  • Use Fluent Assertions: Take advantage of the readable assertion API

  • Inspect Events: Use event inspection for debugging and understanding actor behavior

  • Organize Tests: Structure tests by behavior and keep them focused

  • Handle Async Patterns: Use appropriate timeouts and pattern matching for async operations

The library's zero-dependency design, comprehensive feature set, and integration with Go's testing framework make it the ideal choice for building robust, well-tested actor systems with the Ergo Framework.

Next Steps:

  1. Explore the complete test examples in the framework repository

  2. Start with simple actors and gradually build complexity

  3. Integrate testing into your development workflow

  4. Use the debugging features when tests fail

  5. Share testing patterns with your team

Happy testing!

Last updated

Was this helpful?