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
| Feature | MockClient | Real Actor |
|---|---|---|
| Speed | Instant (in-memory) | Fast (but involves tokio spawn) |
| Determinism | 100% Deterministic | Subject to scheduler |
| State | No real state (expectations) | Real state management |
| Use Case | Unit testing logic around the client | Testing the actor itself or full system |
| Error Injection | Easy (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§
- Action
Expectation Builder - Builder for
actionexpectations. - Create
Expectation Builder - Builder for
createexpectations. - GetExpectation
Builder - Builder for
getexpectations. - Mock
Client - 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