actor_framework/mock.rs
1//! # Mock Framework & Testing Guide
2//!
3//! 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.
4//!
5//! ## When to use Mocks vs Real Actors
6//!
7//! | Feature | MockClient | Real Actor |
8//! |---------|------------|------------|
9//! | **Speed** | Instant (in-memory) | Fast (but involves tokio spawn) |
10//! | **Determinism** | 100% Deterministic | Subject to scheduler |
11//! | **State** | No real state (expectations) | Real state management |
12//! | **Use Case** | Unit testing logic *around* the client | Testing the actor itself or full system |
13//! | **Error Injection** | Easy (`return_err`) | Hard (requires specific state) |
14//!
15//! ## Testing Strategies
16//!
17//! The actor framework supports four distinct testing patterns.
18//!
19//! <details>
20//! <summary><b>Pattern 0: Client Logic Test (Pure Mock)</b></summary>
21//!
22//! **When to use**: Testing complex orchestration logic in your client wrappers without spinning up any actors.
23//!
24//! **Example**:
25//! ```rust
26//! use actor_framework::mock::MockClient;
27//! use actor_framework::{ActorEntity, ResourceClient, ResourceRequest};
28//! use async_trait::async_trait;
29//!
30//! // --- Define a minimal Entity for the test ---
31//! #[derive(Clone, Debug, PartialEq)]
32//! struct User { id: u32, email: String }
33//! #[derive(Debug)] struct UserCreate { email: String }
34//! #[derive(Debug)] struct UserUpdate;
35//! #[derive(Debug)] enum UserAction {}
36//! #[derive(Debug, thiserror::Error)] #[error("User error")] struct UserError;
37//!
38//! #[async_trait]
39//! impl ActorEntity for User {
40//! type Id = u32; type Create = UserCreate; type Update = UserUpdate;
41//! type Action = UserAction; type ActionResult = (); type Context = (); type Error = UserError;
42//! fn from_create_params(id: u32, params: UserCreate) -> Result<Self, Self::Error> {
43//! Ok(Self { id, email: params.email })
44//! }
45//! async fn on_update(&mut self, _: UserUpdate, _: &()) -> Result<(), Self::Error> { Ok(()) }
46//! async fn handle_action(&mut self, _: UserAction, _: &()) -> Result<(), Self::Error> { Ok(()) }
47//! }
48//!
49//! // --- Define a minimal Client Wrapper ---
50//! struct UserClient { client: ResourceClient<User> }
51//! impl UserClient {
52//! fn new(client: ResourceClient<User>) -> Self { Self { client } }
53//! async fn get(&self, id: u32) -> Result<Option<User>, UserError> {
54//! self.client.get(id).await.map_err(|_| UserError)
55//! }
56//! }
57//!
58//! impl User {
59//! fn new(id: u32, email: &str) -> Self { Self { id, email: email.to_string() } }
60//! }
61//!
62//! #[tokio::main]
63//! async fn main() {
64//! // 1. Setup Mocks
65//! let mut user_mock = MockClient::<User>::new();
66//! user_mock.expect_get(1)
67//! .return_ok(Some(User::new(1, "test@example.com")));
68//!
69//! // 2. Create Client with Mocks
70//! let user_client = UserClient::new(user_mock.client());
71//!
72//! // 3. Test Logic
73//! let user = user_client.get(1).await.unwrap();
74//! assert_eq!(user.unwrap().email, "test@example.com");
75//! }
76//! ```
77//! </details>
78//!
79//! <details>
80//! <summary><b>Pattern 1: Single Actor Test (Fast, Isolated)</b></summary>
81//!
82//! **When to use**: Testing a single actor's logic in isolation.
83//!
84//! **Example**:
85//! ```rust
86//! use actor_framework::{ActorEntity, ResourceActor, ResourceClient};
87//! use async_trait::async_trait;
88//!
89//! // --- Define Entity ---
90//! #[derive(Clone, Debug)] struct Product { id: u32, stock: u32 }
91//! #[derive(Debug)] struct ProductCreate { stock: u32 }
92//! #[derive(Debug)] struct ProductUpdate;
93//! #[derive(Debug)] enum ProductAction { CheckStock }
94//! #[derive(Debug, thiserror::Error)] #[error("Err")] struct ProductError;
95//!
96//! #[async_trait]
97//! impl ActorEntity for Product {
98//! type Id = u32; type Create = ProductCreate; type Update = ProductUpdate;
99//! type Action = ProductAction; type ActionResult = u32; type Context = (); type Error = ProductError;
100//! fn from_create_params(id: u32, params: ProductCreate) -> Result<Self, Self::Error> {
101//! Ok(Self { id, stock: params.stock })
102//! }
103//! async fn on_update(&mut self, _: ProductUpdate, _: &()) -> Result<(), Self::Error> { Ok(()) }
104//! async fn handle_action(&mut self, action: ProductAction, _: &()) -> Result<u32, Self::Error> {
105//! match action { ProductAction::CheckStock => Ok(self.stock) }
106//! }
107//! }
108//!
109//! #[tokio::main]
110//! async fn main() {
111//! let (actor, client) = ResourceActor::<Product>::new(10);
112//! tokio::spawn(actor.run(()));
113//!
114//! let params = ProductCreate { stock: 100 };
115//! let id = client.create(params).await.unwrap();
116//! let stock = client.perform_action(id, ProductAction::CheckStock).await.unwrap();
117//! assert_eq!(stock, 100);
118//! }
119//! ```
120//! </details>
121//!
122//! <details>
123//! <summary><b>Pattern 2: Actor with Mocked Dependencies (Sweet Spot)</b></summary>
124//!
125//! **When to use**: Testing an actor that depends on other actors, but you want to isolate the actor under test.
126//!
127//! **Example**:
128//! ```text
129//! This example requires multiple actors and is verbose to implement inline.
130//! See tests/order_actor_test.rs in the actor-recipe-app crate for a full example.
131//! ```
132//! </details>
133//!
134//! <details>
135//! <summary><b>Pattern 3: Full System Integration Test (Comprehensive)</b></summary>
136//!
137//! **When to use**: Testing the entire system working together, end-to-end flows, concurrency.
138//!
139//! See the `test_full_order_system_integration` function in `tests/integration_test.rs` for comprehensive examples.
140//! </details>
141//!
142//! ## Testing Failure Scenarios
143//!
144//! 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).
145//!
146//! ```rust
147//! use actor_framework::mock::MockClient;
148//! use actor_framework::{ActorEntity, FrameworkError};
149//! use async_trait::async_trait;
150//!
151//! #[derive(Clone, Debug)] struct User { id: u32 }
152//! #[derive(Debug)] struct UserCreate;
153//! #[derive(Debug)] struct UserUpdate;
154//! #[derive(Debug)] enum UserAction {}
155//! #[derive(Debug, thiserror::Error)] #[error("Err")] struct UserError;
156//!
157//! #[async_trait]
158//! impl ActorEntity for User {
159//! type Id = u32; type Create = UserCreate; type Update = UserUpdate;
160//! type Action = UserAction; type ActionResult = (); type Context = (); type Error = UserError;
161//! fn from_create_params(id: u32, _: UserCreate) -> Result<Self, Self::Error> { Ok(Self { id }) }
162//! async fn on_update(&mut self, _: UserUpdate, _: &()) -> Result<(), Self::Error> { Ok(()) }
163//! async fn handle_action(&mut self, _: UserAction, _: &()) -> Result<(), Self::Error> { Ok(()) }
164//! }
165//!
166//! #[tokio::main]
167//! async fn main() {
168//! let mut mock = MockClient::<User>::new();
169//! let client = mock.client();
170//!
171//! // Simulate a downstream failure
172//! mock.expect_get(1)
173//! .return_err(FrameworkError::ActorClosed);
174//!
175//! // Verify your code handles it gracefully
176//! let result = client.get(1).await;
177//! assert!(matches!(result, Err(FrameworkError::ActorClosed)));
178//! }
179//! ```
180//!
181//! ## Advanced: Test-Only Actions
182//!
183//! <details>
184//! <summary><b>How to use Feature Flags for Testing</b></summary>
185//!
186//! Sometimes you need to inspect internal actor state for testing. Use a Cargo **feature flag** (`testing`)
187//! instead of `#[cfg(test)]` so it works with integration tests.
188//!
189//! ```toml
190//! [features]
191//! testing = []
192//! ```
193//!
194//! Then guard your test-only actions:
195//!
196//! ```rust,ignore
197//! pub enum ProductAction {
198//! #[cfg(feature = "testing")]
199//! GetInternalState,
200//! }
201//! ```
202//! </details>
203//!
204//! ## Mocking Utilities
205//!
206//! Use [`create_mock_client`] to get a client and a receiver, or use the fluent [`MockClient`] API.
207
208use crate::client::ResourceClient;
209use crate::entity::ActorEntity;
210use crate::error::FrameworkError;
211use crate::message::ResourceRequest;
212use std::collections::VecDeque;
213use std::sync::{Arc, Mutex};
214use tokio::sync::mpsc;
215
216// =============================================================================
217// EXPECTATION BUILDER API
218// =============================================================================
219
220/// Represents an expected request to the mock client.
221///
222/// This enum is used internally by `MockClient` to track what requests
223/// are expected and what responses should be returned.
224#[allow(dead_code)] // Future features: Update, Delete, Action expectations
225enum Expectation<T: ActorEntity> {
226 Get {
227 id: T::Id,
228 response: Result<Option<T>, FrameworkError>,
229 },
230 Create {
231 response: Result<T::Id, FrameworkError>,
232 },
233 Update {
234 id: T::Id,
235 response: Result<T, FrameworkError>,
236 },
237 Delete {
238 id: T::Id,
239 response: Result<(), FrameworkError>,
240 },
241 Action {
242 id: T::Id,
243 response: Result<T::ActionResult, FrameworkError>,
244 },
245}
246
247/// A mock client with expectation tracking for fluent testing.
248///
249/// # Example
250/// ```ignore
251/// let mut mock = MockClient::<User>::new();
252/// mock.expect_get("user_1".to_string()).return_ok(Some(user));
253/// mock.expect_create().return_ok("user_2".to_string());
254///
255/// let client = mock.client();
256/// // Use client in tests...
257/// mock.verify(); // Ensures all expectations were met
258/// ```
259pub struct MockClient<T: ActorEntity> {
260 client: ResourceClient<T>,
261 expectations: Arc<Mutex<VecDeque<Expectation<T>>>>,
262 _handle: tokio::task::JoinHandle<()>,
263}
264
265impl<T: ActorEntity + Send + 'static> Default for MockClient<T>
266where
267 T::Id: Send,
268 T::Create: Send,
269 T::Update: Send,
270 T::Action: Send,
271 T::ActionResult: Send,
272{
273 fn default() -> Self {
274 Self::new()
275 }
276}
277
278impl<T: ActorEntity + Send + 'static> MockClient<T>
279where
280 T::Id: Send,
281 T::Create: Send,
282 T::Update: Send,
283 T::Action: Send,
284 T::ActionResult: Send,
285{
286 /// Creates a new mock client with no expectations.
287 pub fn new() -> Self {
288 let (sender, mut receiver) = mpsc::channel::<ResourceRequest<T>>(100);
289 let expectations = Arc::new(Mutex::new(VecDeque::new()));
290 let expectations_clone = expectations.clone();
291
292 // Spawn background task to handle requests
293 let handle = tokio::spawn(async move {
294 while let Some(request) = receiver.recv().await {
295 let mut exps = expectations_clone.lock().unwrap();
296 let expectation = exps.pop_front();
297 drop(exps); // Release lock before async operations
298
299 match (request, expectation) {
300 (
301 ResourceRequest::Get { id: _, respond_to },
302 Some(Expectation::Get { id: _, response }),
303 ) => {
304 let _ = respond_to.send(response);
305 }
306 (
307 ResourceRequest::Create {
308 params: _,
309 respond_to,
310 },
311 Some(Expectation::Create { response }),
312 ) => {
313 let _ = respond_to.send(response);
314 }
315 (
316 ResourceRequest::Update {
317 id: _,
318 update: _,
319 respond_to,
320 },
321 Some(Expectation::Update { id: _, response }),
322 ) => {
323 let _ = respond_to.send(response);
324 }
325 (
326 ResourceRequest::Delete { id: _, respond_to },
327 Some(Expectation::Delete { id: _, response }),
328 ) => {
329 let _ = respond_to.send(response);
330 }
331 (
332 ResourceRequest::Action {
333 id: _,
334 action: _,
335 respond_to,
336 },
337 Some(Expectation::Action { id: _, response }),
338 ) => {
339 let _ = respond_to.send(response);
340 }
341 _ => {
342 panic!("Unexpected request or expectation mismatch");
343 }
344 }
345 }
346 });
347
348 Self {
349 client: ResourceClient::new(sender),
350 expectations,
351 _handle: handle,
352 }
353 }
354
355 /// Returns the client for use in tests.
356 pub fn client(&self) -> ResourceClient<T> {
357 self.client.clone()
358 }
359
360 /// Expects a `get` operation.
361 pub fn expect_get(&mut self, id: T::Id) -> GetExpectationBuilder<T> {
362 GetExpectationBuilder {
363 id,
364 expectations: self.expectations.clone(),
365 }
366 }
367
368 /// Expects a `create` operation.
369 pub fn expect_create(&mut self) -> CreateExpectationBuilder<T> {
370 CreateExpectationBuilder {
371 expectations: self.expectations.clone(),
372 }
373 }
374
375 /// Expects an `action` operation.
376 pub fn expect_action(&mut self, id: T::Id) -> ActionExpectationBuilder<T> {
377 ActionExpectationBuilder {
378 id,
379 expectations: self.expectations.clone(),
380 }
381 }
382
383 /// Verifies that all expectations were met.
384 pub fn verify(&self) {
385 let exps = self.expectations.lock().unwrap();
386 if !exps.is_empty() {
387 panic!("Not all expectations were met. {} remaining", exps.len());
388 }
389 }
390}
391
392/// Builder for `get` expectations.
393pub struct GetExpectationBuilder<T: ActorEntity> {
394 id: T::Id,
395 expectations: Arc<Mutex<VecDeque<Expectation<T>>>>,
396}
397
398impl<T: ActorEntity> GetExpectationBuilder<T> {
399 /// Sets the expectation to return a successful result.
400 pub fn return_ok(self, value: Option<T>) {
401 let mut exps = self.expectations.lock().unwrap();
402 exps.push_back(Expectation::Get {
403 id: self.id,
404 response: Ok(value),
405 });
406 }
407
408 /// Sets the expectation to return an error.
409 pub fn return_err(self, error: FrameworkError) {
410 let mut exps = self.expectations.lock().unwrap();
411 exps.push_back(Expectation::Get {
412 id: self.id,
413 response: Err(error),
414 });
415 }
416}
417
418/// Builder for `create` expectations.
419pub struct CreateExpectationBuilder<T: ActorEntity> {
420 expectations: Arc<Mutex<VecDeque<Expectation<T>>>>,
421}
422
423impl<T: ActorEntity> CreateExpectationBuilder<T> {
424 /// Sets the expectation to return a successful result.
425 pub fn return_ok(self, id: T::Id) {
426 let mut exps = self.expectations.lock().unwrap();
427 exps.push_back(Expectation::Create { response: Ok(id) });
428 }
429
430 /// Sets the expectation to return an error.
431 pub fn return_err(self, error: FrameworkError) {
432 let mut exps = self.expectations.lock().unwrap();
433 exps.push_back(Expectation::Create {
434 response: Err(error),
435 });
436 }
437}
438
439/// Builder for `action` expectations.
440pub struct ActionExpectationBuilder<T: ActorEntity> {
441 id: T::Id,
442 expectations: Arc<Mutex<VecDeque<Expectation<T>>>>,
443}
444
445impl<T: ActorEntity> ActionExpectationBuilder<T> {
446 /// Sets the expectation to return a successful result.
447 pub fn return_ok(self, result: T::ActionResult) {
448 let mut exps = self.expectations.lock().unwrap();
449 exps.push_back(Expectation::Action {
450 id: self.id,
451 response: Ok(result),
452 });
453 }
454
455 /// Sets the expectation to return an error.
456 pub fn return_err(self, error: FrameworkError) {
457 let mut exps = self.expectations.lock().unwrap();
458 exps.push_back(Expectation::Action {
459 id: self.id,
460 response: Err(error),
461 });
462 }
463}
464
465// =============================================================================
466// LEGACY HELPERS (for backward compatibility)
467// =============================================================================
468
469/// Creates a mock client and a receiver for asserting requests.
470///
471/// # Testing Strategy
472/// In unit/integration tests, we don't want to spin up a full `ResourceActor` if we are just
473/// testing the *Client* logic (e.g., `OrderClient`).
474///
475/// Instead, we create a "Mock Client". This client sends messages to a channel we control (`receiver`).
476/// We can then inspect the messages arriving on that channel and assert they are correct.
477/// This allows us to simulate the Actor's behavior (success, failure, delays) deterministically.
478///
479/// **Note**: Consider using [`MockClient`] for a more fluent API.
480pub fn create_mock_client<T: ActorEntity>(
481 buffer_size: usize,
482) -> (ResourceClient<T>, mpsc::Receiver<ResourceRequest<T>>) {
483 let (sender, receiver) = mpsc::channel(buffer_size);
484 (ResourceClient::new(sender), receiver)
485}
486
487/// Helper to verify that the next message is a Create request
488pub async fn expect_create<T: ActorEntity>(
489 receiver: &mut mpsc::Receiver<ResourceRequest<T>>,
490) -> Option<(
491 T::Create,
492 tokio::sync::oneshot::Sender<Result<T::Id, FrameworkError>>,
493)> {
494 match receiver.recv().await {
495 Some(ResourceRequest::Create { params, respond_to }) => Some((params, respond_to)),
496 _ => None,
497 }
498}
499
500/// Helper to verify that the next message is a Get request
501pub async fn expect_get<T: ActorEntity>(
502 receiver: &mut mpsc::Receiver<ResourceRequest<T>>,
503) -> Option<(
504 T::Id,
505 tokio::sync::oneshot::Sender<Result<Option<T>, FrameworkError>>,
506)> {
507 match receiver.recv().await {
508 Some(ResourceRequest::Get { id, respond_to }) => Some((id, respond_to)),
509 _ => None,
510 }
511}
512
513/// Helper to verify that the next message is an Action request
514pub async fn expect_action<T: ActorEntity>(
515 receiver: &mut mpsc::Receiver<ResourceRequest<T>>,
516) -> Option<(
517 T::Id,
518 T::Action,
519 tokio::sync::oneshot::Sender<Result<T::ActionResult, FrameworkError>>,
520)> {
521 match receiver.recv().await {
522 Some(ResourceRequest::Action {
523 id,
524 action,
525 respond_to,
526 }) => Some((id, action, respond_to)),
527 _ => None,
528 }
529}
530
531#[cfg(test)]
532mod tests {
533 use super::*;
534 use crate::entity::ActorEntity;
535 use async_trait::async_trait;
536
537 #[derive(Clone, Debug, PartialEq)]
538 struct User {
539 id: u32,
540 name: String,
541 email: String,
542 }
543
544 #[derive(Debug)]
545 struct UserCreate {
546 name: String,
547 email: String,
548 }
549
550 #[derive(Debug)]
551 struct UserUpdate;
552
553 #[derive(Debug)]
554 enum UserAction {}
555
556 #[derive(Debug, thiserror::Error)]
557 #[error("User error")]
558 struct UserError;
559
560 #[async_trait]
561 impl ActorEntity for User {
562 type Id = u32;
563 type Create = UserCreate;
564 type Update = UserUpdate;
565 type Action = UserAction;
566 type ActionResult = ();
567 type Context = ();
568 type Error = UserError;
569
570 fn from_create_params(id: u32, params: UserCreate) -> Result<Self, Self::Error> {
571 Ok(Self {
572 id,
573 name: params.name,
574 email: params.email,
575 })
576 }
577 async fn on_update(
578 &mut self,
579 _update: UserUpdate,
580 _ctx: &Self::Context,
581 ) -> Result<(), Self::Error> {
582 Ok(())
583 }
584
585 async fn handle_action(
586 &mut self,
587 _action: UserAction,
588 _ctx: &Self::Context,
589 ) -> Result<(), Self::Error> {
590 Ok(())
591 }
592 }
593
594 impl User {
595 fn new(id: u32, email: &str) -> Self {
596 Self {
597 id,
598 name: "Test User".to_string(),
599 email: email.to_string(),
600 }
601 }
602 }
603
604 #[tokio::test]
605 async fn test_mock_client() {
606 let (client, mut receiver) = create_mock_client::<User>(10);
607
608 // Test Create
609 let create_task = tokio::spawn(async move {
610 let user = UserCreate {
611 name: "Test".to_string(),
612 email: "test@example.com".to_string(),
613 };
614 client.create(user).await
615 });
616
617 let (payload, responder) = expect_create(&mut receiver)
618 .await
619 .expect("Expected Create request");
620 assert_eq!(payload.name, "Test");
621 responder.send(Ok(1)).unwrap();
622
623 let result = create_task.await.unwrap();
624 assert!(matches!(result, Ok(id) if id == 1));
625 }
626
627 #[tokio::test]
628 async fn test_mock_client_with_expectations() {
629 // Create mock with fluent expectation API
630 let mut mock = MockClient::<User>::new();
631
632 // Set up expectations
633 mock.expect_create().return_ok(1);
634 mock.expect_get(1)
635 .return_ok(Some(User::new(1, "test@example.com")));
636
637 let client = mock.client();
638
639 // Execute operations
640 let user = UserCreate {
641 name: "Test".to_string(),
642 email: "test@example.com".to_string(),
643 };
644 let id = client.create(user).await.unwrap();
645 assert_eq!(id, 1);
646
647 let fetched = client.get(1).await.unwrap();
648 assert!(fetched.is_some());
649 assert_eq!(fetched.unwrap().email, "test@example.com");
650
651 // Verify all expectations were met
652 mock.verify();
653 }
654}