Module mock

Module mock 

Source
Expand description

§Mock Framework & Testing Guide

The MockClient<T> type implements the same ResourceClient<T> API as the production client but operates entirely in‑memory. It lets you set expectations and return values for unit tests, enabling fast, deterministic testing of client logic without spawning any actors.

§When to use Mocks vs Real Actors

FeatureMockClientReal Actor
SpeedInstant (in-memory)Fast (but involves tokio spawn)
Determinism100% DeterministicSubject to scheduler
StateNo real state (expectations)Real state management
Use CaseUnit testing logic around the clientTesting the actor itself or full system
Error InjectionEasy (return_err)Hard (requires specific state)

§Testing Strategies

The actor framework supports four distinct testing patterns.

Pattern 0: Client Logic Test (Pure Mock)

When to use: Testing complex orchestration logic in your client wrappers without spinning up any actors.

Example:

use actor_framework::mock::MockClient;
use actor_framework::{ActorEntity, ResourceClient, ResourceRequest};
use async_trait::async_trait;

// --- Define a minimal Entity for the test ---
#[derive(Clone, Debug, PartialEq)]
struct User { id: u32, email: String }
#[derive(Debug)] struct UserCreate { email: String }
#[derive(Debug)] struct UserUpdate;
#[derive(Debug)] enum UserAction {}
#[derive(Debug, thiserror::Error)] #[error("User error")] struct UserError;

#[async_trait]
impl ActorEntity for User {
    type Id = u32; type Create = UserCreate; type Update = UserUpdate;
    type Action = UserAction; type ActionResult = (); type Context = (); type Error = UserError;
    fn from_create_params(id: u32, params: UserCreate) -> Result<Self, Self::Error> {
        Ok(Self { id, email: params.email })
    }
    async fn on_update(&mut self, _: UserUpdate, _: &()) -> Result<(), Self::Error> { Ok(()) }
    async fn handle_action(&mut self, _: UserAction, _: &()) -> Result<(), Self::Error> { Ok(()) }
}

// --- Define a minimal Client Wrapper ---
struct UserClient { client: ResourceClient<User> }
impl UserClient {
    fn new(client: ResourceClient<User>) -> Self { Self { client } }
    async fn get(&self, id: u32) -> Result<Option<User>, UserError> {
        self.client.get(id).await.map_err(|_| UserError)
    }
}

impl User {
    fn new(id: u32, email: &str) -> Self { Self { id, email: email.to_string() } }
}

#[tokio::main]
async fn main() {
    // 1. Setup Mocks
    let mut user_mock = MockClient::<User>::new();
    user_mock.expect_get(1)
        .return_ok(Some(User::new(1, "test@example.com")));

    // 2. Create Client with Mocks
    let user_client = UserClient::new(user_mock.client());
     
    // 3. Test Logic
    let user = user_client.get(1).await.unwrap();
    assert_eq!(user.unwrap().email, "test@example.com");
}
Pattern 1: Single Actor Test (Fast, Isolated)

When to use: Testing a single actor’s logic in isolation.

Example:

use actor_framework::{ActorEntity, ResourceActor, ResourceClient};
use async_trait::async_trait;

// --- Define Entity ---
#[derive(Clone, Debug)] struct Product { id: u32, stock: u32 }
#[derive(Debug)] struct ProductCreate { stock: u32 }
#[derive(Debug)] struct ProductUpdate;
#[derive(Debug)] enum ProductAction { CheckStock }
#[derive(Debug, thiserror::Error)] #[error("Err")] struct ProductError;

#[async_trait]
impl ActorEntity for Product {
    type Id = u32; type Create = ProductCreate; type Update = ProductUpdate;
    type Action = ProductAction; type ActionResult = u32; type Context = (); type Error = ProductError;
    fn from_create_params(id: u32, params: ProductCreate) -> Result<Self, Self::Error> {
        Ok(Self { id, stock: params.stock })
    }
    async fn on_update(&mut self, _: ProductUpdate, _: &()) -> Result<(), Self::Error> { Ok(()) }
    async fn handle_action(&mut self, action: ProductAction, _: &()) -> Result<u32, Self::Error> {
        match action { ProductAction::CheckStock => Ok(self.stock) }
    }
}

#[tokio::main]
async fn main() {
    let (actor, client) = ResourceActor::<Product>::new(10);
    tokio::spawn(actor.run(()));
     
    let params = ProductCreate { stock: 100 };
    let id = client.create(params).await.unwrap();
    let stock = client.perform_action(id, ProductAction::CheckStock).await.unwrap();
    assert_eq!(stock, 100);
}
Pattern 2: Actor with Mocked Dependencies (Sweet Spot)

When to use: Testing an actor that depends on other actors, but you want to isolate the actor under test.

Example:

This example requires multiple actors and is verbose to implement inline.
See tests/order_actor_test.rs in the actor-recipe-app crate for a full example.
Pattern 3: Full System Integration Test (Comprehensive)

When to use: Testing the entire system working together, end-to-end flows, concurrency.

See the test_full_order_system_integration function in tests/integration_test.rs for comprehensive examples.

§Testing Failure Scenarios

One of the biggest advantages of MockClient is the ability to simulate errors that are hard to reproduce with real actors (e.g., database timeouts, network partitions).

use actor_framework::mock::MockClient;
use actor_framework::{ActorEntity, FrameworkError};
use async_trait::async_trait;

#[derive(Clone, Debug)] struct User { id: u32 }
#[derive(Debug)] struct UserCreate;
#[derive(Debug)] struct UserUpdate;
#[derive(Debug)] enum UserAction {}
#[derive(Debug, thiserror::Error)] #[error("Err")] struct UserError;

#[async_trait]
impl ActorEntity for User {
    type Id = u32; type Create = UserCreate; type Update = UserUpdate;
    type Action = UserAction; type ActionResult = (); type Context = (); type Error = UserError;
    fn from_create_params(id: u32, _: UserCreate) -> Result<Self, Self::Error> { Ok(Self { id }) }
    async fn on_update(&mut self, _: UserUpdate, _: &()) -> Result<(), Self::Error> { Ok(()) }
    async fn handle_action(&mut self, _: UserAction, _: &()) -> Result<(), Self::Error> { Ok(()) }
}

#[tokio::main]
async fn main() {
    let mut mock = MockClient::<User>::new();
    let client = mock.client();

    // Simulate a downstream failure
    mock.expect_get(1)
        .return_err(FrameworkError::ActorClosed);

    // Verify your code handles it gracefully
    let result = client.get(1).await;
    assert!(matches!(result, Err(FrameworkError::ActorClosed)));
}

§Advanced: Test-Only Actions

How to use Feature Flags for Testing

Sometimes you need to inspect internal actor state for testing. Use a Cargo feature flag (testing) instead of #[cfg(test)] so it works with integration tests.

[features]
testing = []

Then guard your test-only actions:

ⓘ
pub enum ProductAction {
    #[cfg(feature = "testing")]
    GetInternalState,
}

§Mocking Utilities

Use create_mock_client to get a client and a receiver, or use the fluent MockClient API.

Structs§

ActionExpectationBuilder
Builder for action expectations.
CreateExpectationBuilder
Builder for create expectations.
GetExpectationBuilder
Builder for get expectations.
MockClient
A mock client with expectation tracking for fluent testing.

Enums§

Expectation 🔒
Represents an expected request to the mock client.

Functions§

create_mock_client
Creates a mock client and a receiver for asserting requests.
expect_action
Helper to verify that the next message is an Action request
expect_create
Helper to verify that the next message is a Create request
expect_get
Helper to verify that the next message is a Get request