actix_actor_expect/
lib.rs

1use std::any::Any;
2use std::marker::PhantomData;
3use std::sync::{Arc, Mutex};
4
5use actix::actors::mocker::Mocker;
6use actix::{Actor, Addr, Context, Message};
7
8type ReceivedCallsLog = Arc<Mutex<Vec<Box<dyn Any>>>>;
9
10/// Utility for unit testing actix actors.
11/// Helper for reducing the boilerplate when unit testing actix actors.
12/// Configures a  mocker actor to expect a particular incoming command `I` and to respond with provided outgoing response `O`.
13pub struct ActorExpect<T: Sized + Unpin + 'static, Error: 'static> {
14    pub addr: Addr<Mocker<T>>,
15    received_calls: ReceivedCallsLog,
16    phantom_error_data: PhantomData<Error>,
17}
18
19impl<T: Sized + Unpin + 'static, Error: 'static> ActorExpect<T, Error> {
20    /// Creates a mocker that accepts incoming and returns outgoing message.
21    /// If other messages are received, default_outgoing message is returned.
22    ///
23    /// # Arguments
24    /// * `incoming` - incoming message for actor.
25    /// * `outgoing` - response message for actor when incoming received.
26    /// * `default_outgoing` - default response message for anything other than `incoming`.
27    ///                        If `None` is set, actor mailbox is closed on unsupported message.
28    pub fn expect_send<I, O>(incoming: I, outgoing: O, default_outgoing: Option<O>) -> Self
29    where
30        I: 'static + Clone + PartialEq + Message + Send,
31        I::Result: Send,
32        O: 'static + Clone + PartialEq,
33    {
34        let log: ReceivedCallsLog = Arc::new(Mutex::new(vec![]));
35        let cloned_log = log.clone(); // cloned right away to avoid error borrow of moved value
36        let mocker = Mocker::<T>::mock(Box::new(move |msg, ctx| {
37            let result: Option<Result<O, Error>> = ActorExpect::<T, Error>::process_messaging(
38                &cloned_log,
39                msg,
40                incoming.clone(),
41                outgoing.clone(),
42                default_outgoing.clone(),
43                ctx,
44            );
45
46            let boxed_result: Box<Option<Result<O, Error>>> = Box::new(result);
47            boxed_result
48        }));
49
50        let addr = mocker.start();
51
52        Self {
53            addr,
54            received_calls: log.clone(),
55            phantom_error_data: PhantomData,
56        }
57    }
58
59    /// Creates an actor that is a placeholder:
60    /// - it doesn't accept sent messages.
61    /// - if message is received, inbox closes right away.
62    pub fn placeholder<O: 'static + Clone + PartialEq>() -> Self {
63        let mocker = Mocker::<T>::mock(Box::new(move |_msg, _ctx| {
64            let result: Option<Result<O, Error>> = None;
65            Box::new(result)
66        }));
67        let addr = mocker.start();
68        Self {
69            addr,
70            received_calls: Arc::new(Mutex::new(vec![])),
71            phantom_error_data: PhantomData,
72        }
73    }
74
75    /// Returns a total number of calls that the mocker received.
76    pub fn total_calls(&self) -> usize {
77        let received_calls = self
78            .received_calls
79            .lock()
80            .expect("Received calls log error!");
81        received_calls.len()
82    }
83
84    /// Returns a total number of calls that the mocker received for msg type or variant.
85    ///
86    /// # Arguments
87    /// * `msg` - message for actor
88    pub fn calls_of_variant<MSG: Any + 'static + PartialEq>(&self, msg: MSG) -> usize {
89        let mut count = 0;
90        for item in self
91            .received_calls
92            .lock()
93            .unwrap_or_else(|_| panic!("Received calls log error!"))
94            .iter()
95        {
96            let it = item.as_ref().downcast_ref::<MSG>();
97            if let Some(message_kind) = it {
98                if msg == *message_kind {
99                    count += 1
100                }
101            }
102        }
103        count
104    }
105
106    fn process_messaging<I: 'static + Clone + PartialEq, O: 'static + Clone + PartialEq>(
107        log: &ReceivedCallsLog,
108        msg: Box<dyn Any>,
109        incoming: I,
110        outgoing: O,
111        default_outgoing: Option<O>,
112        _ctx: &mut Context<Mocker<T>>,
113    ) -> Option<Result<O, Error>> {
114        let command: &I = msg
115            .downcast_ref::<I>()
116            .unwrap_or_else(|| panic!("Cannot downcast command!"));
117        let _ = log
118            .lock()
119            .unwrap_or_else(|_| panic!("Received calls log error!"))
120            .push(Box::new(command.clone()));
121        if command.clone() == incoming {
122            Some(Ok(outgoing))
123        } else {
124            default_outgoing.map(Ok)
125        }
126    }
127}