crux_core/
testing.rs

1//! Testing support for unit testing Crux apps.
2use anyhow::Result;
3use std::{collections::VecDeque, sync::Arc};
4
5use crate::{
6    capability::{
7        channel::Receiver, executor_and_spawner, CommandSpawner, Operation, ProtoContext,
8        QueuingExecutor,
9    },
10    Command, Request, WithContext,
11};
12
13/// `AppTester` is a simplified execution environment for Crux apps for use in
14/// tests.
15///
16/// Please note that the `AppTester` is strictly no longer required now that Crux
17/// has a new [`Command`] API. To test apps without the `AppTester`, you can call
18/// the `update` method on your app directly, and then inspect the effects
19/// returned by the command. For examples of how to do this, consult any of the
20/// [examples in the Crux repository](https://github.com/redbadger/crux/tree/master/examples).
21/// The `AppTester` is still provided for backwards compatibility, and to allow you to
22/// migrate to the new API without changing the tests,
23/// giving you increased confidence in your refactor.
24///
25/// Create an instance of `AppTester` with your `App` and an `Effect` type
26/// using [`AppTester::default`].
27///
28/// for example:
29///
30/// ```rust,ignore
31/// let app = AppTester::<ExampleApp, ExampleEffect>::default();
32/// ```
33pub struct AppTester<App>
34where
35    App: crate::App,
36{
37    app: App,
38    capabilities: App::Capabilities,
39    context: Arc<AppContext<App::Effect, App::Event>>,
40    command_spawner: CommandSpawner<App::Effect, App::Event>,
41}
42
43struct AppContext<Ef, Ev> {
44    commands: Receiver<Ef>,
45    events: Receiver<Ev>,
46    executor: QueuingExecutor,
47}
48
49impl<App> AppTester<App>
50where
51    App: crate::App,
52{
53    /// Create an `AppTester` instance for an existing app instance. This can be used if your App
54    /// has a constructor other than `Default`, for example when used as a child app and expecting
55    /// configuration from the parent
56    pub fn new(app: App) -> Self
57    where
58        App::Capabilities: WithContext<App::Event, App::Effect>,
59    {
60        Self {
61            app,
62            ..Default::default()
63        }
64    }
65
66    /// Run the app's `update` function with an event and a model state
67    ///
68    /// You can use the resulting [`Update`] to inspect the effects which were requested
69    /// and potential further events dispatched by capabilities.
70    pub fn update(
71        &self,
72        event: App::Event,
73        model: &mut App::Model,
74    ) -> Update<App::Effect, App::Event> {
75        let command = self.app.update(event, model, &self.capabilities);
76        self.command_spawner.spawn(command);
77
78        self.context.updates()
79    }
80
81    /// Resolve an effect `request` from previous update with an operation output.
82    ///
83    /// This potentially runs the app's `update` function if the effect is completed, and
84    /// produce another `Update`.
85    ///
86    /// # Errors
87    ///
88    /// Errors if the request cannot (or should not) be resolved.
89    pub fn resolve<Op: Operation>(
90        &self,
91        request: &mut Request<Op>,
92        value: Op::Output,
93    ) -> Result<Update<App::Effect, App::Event>> {
94        request.resolve(value)?;
95
96        Ok(self.context.updates())
97    }
98
99    /// Resolve an effect `request` from previous update, then run the resulting event
100    ///
101    /// This helper is useful for the common case where  one expects the effect to resolve
102    /// to exactly one event, which should then be run by the app.
103    ///
104    /// # Panics
105    ///
106    /// Panics if the request cannot be resolved.
107    #[track_caller]
108    pub fn resolve_to_event_then_update<Op: Operation>(
109        &self,
110        request: &mut Request<Op>,
111        value: Op::Output,
112        model: &mut App::Model,
113    ) -> Update<App::Effect, App::Event> {
114        request.resolve(value).expect("failed to resolve request");
115        let event = self.context.updates().expect_one_event();
116        self.update(event, model)
117    }
118
119    /// Run the app's `view` function with a model state
120    pub fn view(&self, model: &App::Model) -> App::ViewModel {
121        self.app.view(model)
122    }
123}
124
125impl<App> Default for AppTester<App>
126where
127    App: crate::App,
128    App::Capabilities: WithContext<App::Event, App::Effect>,
129{
130    fn default() -> Self {
131        let (command_sender, commands) = crate::capability::channel();
132        let (event_sender, events) = crate::capability::channel();
133        let (executor, spawner) = executor_and_spawner();
134        let capability_context = ProtoContext::new(command_sender, event_sender, spawner);
135        let command_spawner = CommandSpawner::new(capability_context.clone());
136
137        Self {
138            app: App::default(),
139            capabilities: App::Capabilities::new_with_context(capability_context),
140            context: Arc::new(AppContext {
141                commands,
142                events,
143                executor,
144            }),
145            command_spawner,
146        }
147    }
148}
149
150impl<App> AsRef<App::Capabilities> for AppTester<App>
151where
152    App: crate::App,
153{
154    fn as_ref(&self) -> &App::Capabilities {
155        &self.capabilities
156    }
157}
158
159impl<Ef, Ev> AppContext<Ef, Ev> {
160    pub fn updates(self: &Arc<Self>) -> Update<Ef, Ev> {
161        self.executor.run_all();
162        let effects = self.commands.drain().collect();
163        let events = self.events.drain().collect();
164
165        Update { effects, events }
166    }
167}
168
169/// Update test helper holds the result of running an app update using [`AppTester::update`]
170/// or resolving a request with [`AppTester::resolve`].
171#[derive(Debug)]
172#[must_use]
173pub struct Update<Ef, Ev> {
174    /// Effects requested from the update run
175    pub effects: Vec<Ef>,
176    /// Events dispatched from the update run
177    pub events: Vec<Ev>,
178}
179
180impl<Ef, Ev> Update<Ef, Ev> {
181    pub fn into_effects(self) -> impl Iterator<Item = Ef> {
182        self.effects.into_iter()
183    }
184
185    pub fn effects(&self) -> impl Iterator<Item = &Ef> {
186        self.effects.iter()
187    }
188
189    pub fn effects_mut(&mut self) -> impl Iterator<Item = &mut Ef> {
190        self.effects.iter_mut()
191    }
192
193    /// Assert that the update contains exactly one effect and zero events,
194    /// and return the effect
195    ///
196    /// # Panics
197    /// Panics if the update contains more than one effect or any events.
198    #[track_caller]
199    #[must_use]
200    pub fn expect_one_effect(mut self) -> Ef {
201        if self.events.is_empty() && self.effects.len() == 1 {
202            self.effects.pop().unwrap()
203        } else {
204            panic!(
205                "Expected one effect but found {} effect(s) and {} event(s)",
206                self.effects.len(),
207                self.events.len()
208            );
209        }
210    }
211
212    /// Assert that the update contains exactly one event and zero effects,
213    /// and return the event
214    ///
215    /// # Panics
216    /// Panics if the update contains more than one event or any effects.
217    #[track_caller]
218    #[must_use]
219    pub fn expect_one_event(mut self) -> Ev {
220        if self.effects.is_empty() && self.events.len() == 1 {
221            self.events.pop().unwrap()
222        } else {
223            panic!(
224                "Expected one event but found {} effect(s) and {} event(s)",
225                self.effects.len(),
226                self.events.len()
227            );
228        }
229    }
230
231    /// Assert that the update contains no effects or events
232    ///
233    /// # Panics
234    /// Panics if the update contains any effects or events.
235    #[track_caller]
236    pub fn assert_empty(self) {
237        if self.effects.is_empty() && self.events.is_empty() {
238            return;
239        }
240        panic!(
241            "Expected empty update but found {} effect(s) and {} event(s)",
242            self.effects.len(),
243            self.events.len()
244        );
245    }
246
247    /// Take effects matching the `predicate` out of the [`Update`]
248    /// and return them, mutating the `Update`
249    pub fn take_effects<P>(&mut self, predicate: P) -> VecDeque<Ef>
250    where
251        P: FnMut(&Ef) -> bool,
252    {
253        let (matching_effects, other_effects) = self.take_effects_partitioned_by(predicate);
254
255        self.effects = other_effects.into_iter().collect();
256
257        matching_effects
258    }
259
260    /// Take all of the effects out of the [`Update`]
261    /// and split them into those matching `predicate` and the rest
262    pub fn take_effects_partitioned_by<P>(&mut self, predicate: P) -> (VecDeque<Ef>, VecDeque<Ef>)
263    where
264        P: FnMut(&Ef) -> bool,
265    {
266        std::mem::take(&mut self.effects)
267            .into_iter()
268            .partition(predicate)
269    }
270}
271
272impl<Effect, Event> Command<Effect, Event>
273where
274    Effect: Send + 'static,
275    Event: Send + 'static,
276{
277    /// Assert that the Command contains _exactly_ one effect and zero events,
278    /// and return the effect
279    ///
280    /// # Panics
281    /// Panics if the command does not contain exactly one effect, or contains any events.
282    #[track_caller]
283    pub fn expect_one_effect(&mut self) -> Effect {
284        assert!(
285            self.events().next().is_none(),
286            "expected only one effect, but found an event"
287        );
288        let mut effects = self.effects();
289        match (effects.next(), effects.next()) {
290            (None, _) => panic!("expected one effect but got none"),
291            (Some(effect), None) => effect,
292            _ => panic!("expected one effect but got more than one"),
293        }
294    }
295}
296
297/// Panics if the pattern doesn't match an `Effect` from the specified `Update`
298///
299/// Like in a `match` expression, the pattern can be optionally followed by `if`
300/// and a guard expression that has access to names bound by the pattern.
301///
302/// # Example
303///
304/// ```
305/// # use crux_core::testing::Update;
306/// # enum Effect { Render(String) };
307/// # enum Event { None };
308/// # let effects = vec![Effect::Render("test".to_string())].into_iter().collect();
309/// # let mut update = Update { effects, events: vec!(Event::None) };
310/// use crux_core::assert_effect;
311/// assert_effect!(update, Effect::Render(_));
312/// ```
313#[macro_export]
314macro_rules! assert_effect {
315    ($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => {
316        assert!($expression.effects().any(|e| matches!(e, $( $pattern )|+ $( if $guard )?)));
317    };
318}