Building Apps

Testing Actors

Actors are inherently testable. Their message-based interface makes it clear what inputs you can send and what behaviors to verify.

Basic Test Setup

Each test creates its own runtime:

#[tokio::test]
async fn test_counter_increments() {
    let mut runtime = ActonApp::launch_async().await;
    let mut counter = runtime.new_actor::<Counter>();

    counter
        .mutate_on::<Increment>(handle_increment)
        .act_on::<PrintCount>(handle_print);

    let handle = counter.start().await;

    handle.send(Increment).await;
    handle.send(Increment).await;
    handle.send(PrintCount).await;

    // Give time for async processing
    tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;

    runtime.shutdown_all().await.ok();
}

Testing with Response Actors

Create a "probe" actor to receive and verify responses:

use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::Arc;

#[acton_actor]
struct Probe {
    received_count: Arc<AtomicI32>,
}

#[acton_message]
struct CountResponse(i32);

#[tokio::test]
async fn test_get_count() {
    let mut runtime = ActonApp::launch_async().await;

    // Create the actor under test
    let mut counter = runtime.new_actor::<Counter>();
    counter
        .mutate_on::<Increment>(|actor, _env| {
            actor.model.count += 1;
            Reply::ready()
        })
        .act_on::<GetCount>(|actor, env| {
            let count = actor.model.count;
            let reply = env.reply_envelope();
            Reply::pending(async move {
                reply.send(CountResponse(count)).await;
            })
        });

    let counter_handle = counter.start().await;

    // Create probe to receive response
    let received = Arc::new(AtomicI32::new(-1));
    let received_clone = received.clone();

    let mut probe = runtime.new_actor::<Probe>();
    probe.model.received_count = received_clone.clone();

    probe.mutate_on::<CountResponse>(|actor, env| {
        let count = env.message().0;
        actor.model.received_count.store(count, Ordering::SeqCst);
        Reply::ready()
    });

    let probe_handle = probe.start().await;

    // Increment counter
    counter_handle.send(Increment).await;
    counter_handle.send(Increment).await;

    // Query count with probe as recipient
    let query = probe_handle.create_envelope(
        Some(counter_handle.reply_address())
    );
    query.send(GetCount).await;

    // Wait for response
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

    assert_eq!(received.load(Ordering::SeqCst), 2);

    runtime.shutdown_all().await.ok();
}

Testing Multiple Actors

Create multiple actors and test their interactions:

#[tokio::test]
async fn test_producer_consumer() {
    let mut runtime = ActonApp::launch_async().await;

    let received = Arc::new(AtomicUsize::new(0));
    let received_clone = received.clone();

    // Consumer tracks received items
    let mut consumer = runtime.new_actor::<Consumer>();
    consumer.model.received = received_clone;
    consumer.mutate_on::<Item>(|actor, _env| {
        actor.model.received.fetch_add(1, Ordering::SeqCst);
        Reply::ready()
    });

    let consumer_handle = consumer.start().await;

    // Producer sends to consumer
    let mut producer = runtime.new_actor::<Producer>();
    producer.model.consumer = Some(consumer_handle.clone());
    producer.mutate_on::<Produce>(|actor, env| {
        let count = env.message().count;
        let consumer = actor.model.consumer.clone().unwrap();

        Reply::pending(async move {
            for _ in 0..count {
                consumer.send(Item).await;
            }
        })
    });

    let producer_handle = producer.start().await;

    producer_handle.send(Produce { count: 5 }).await;

    // Wait for async processing
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

    assert_eq!(received.load(Ordering::SeqCst), 5);

    runtime.shutdown_all().await.ok();
}

Test Helpers

Create helper functions for common setup:

async fn setup_counter(runtime: &mut ActorRuntime) -> ActorHandle {
    let mut counter = runtime.new_actor::<Counter>();
    counter
        .mutate_on::<Increment>(handle_increment)
        .act_on::<GetCount>(handle_get);
    counter.start().await
}

#[tokio::test]
async fn test_with_helper() {
    let mut runtime = ActonApp::launch_async().await;
    let counter = setup_counter(&mut runtime).await;

    counter.send(Increment).await;
    // Test logic here

    runtime.shutdown_all().await.ok();
}

Testing Pub/Sub

Test broker-based messaging:

#[tokio::test]
async fn test_broadcast() {
    let mut runtime = ActonApp::launch_async().await;
    let broker = runtime.broker();

    let received = Arc::new(AtomicUsize::new(0));

    // Create subscribers
    for _ in 0..3 {
        let received_clone = received.clone();
        let mut subscriber = runtime.new_actor::<Subscriber>();
        subscriber.model.counter = received_clone;

        subscriber.mutate_on::<Event>(|actor, _env| {
            actor.model.counter.fetch_add(1, Ordering::SeqCst);
            Reply::ready()
        });

        // Subscribe before starting
        subscriber.handle().subscribe::<Event>().await;
        subscriber.start().await;
    }

    // Broadcast event
    broker.broadcast(Event).await;

    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

    // All 3 subscribers should have received the event
    assert_eq!(received.load(Ordering::SeqCst), 3);

    runtime.shutdown_all().await.ok();
}

Avoiding Flaky Tests

Use atomic counters for verification

// GOOD: Use atomics for cross-actor state verification
let count = Arc::new(AtomicI32::new(0));
// ... share with probe actor ...
assert_eq!(count.load(Ordering::SeqCst), expected);

Allow time for async processing

// Give messages time to be processed
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

Isolate each test

// Each test gets its own runtime
#[tokio::test]
async fn test_one() {
    let mut runtime = ActonApp::launch_async().await;  // Isolated
    // ...
    runtime.shutdown_all().await.ok();
}

#[tokio::test]
async fn test_two() {
    let mut runtime = ActonApp::launch_async().await;  // Isolated
    // ...
    runtime.shutdown_all().await.ok();
}

Summary

  • Use #[tokio::test] for async tests
  • Each test creates its own ActorRuntime
  • Use probe actors with atomic counters to verify responses
  • Create helper functions for common setup
  • Allow time for async message processing
  • Clean up with shutdown_all()

Continue Learning

You've covered the Building Apps section:

  • Parent-child actors
  • Request-response patterns
  • Error handling
  • Testing strategies

Continue to Advanced for topics like IPC and performance.

Previous
Error handling