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    pub fn resolve<Op: Operation>(
86        &self,
87        request: &mut Request<Op>,
88        value: Op::Output,
89    ) -> Result<Update<App::Effect, App::Event>> {
90        request.resolve(value)?;
91
92        Ok(self.context.updates())
93    }
94
95    /// Resolve an effect `request` from previous update, then run the resulting event
96    ///
97    /// This helper is useful for the common case where  one expects the effect to resolve
98    /// to exactly one event, which should then be run by the app.
99    pub fn resolve_to_event_then_update<Op: Operation>(
100        &self,
101        request: &mut Request<Op>,
102        value: Op::Output,
103        model: &mut App::Model,
104    ) -> Update<App::Effect, App::Event> {
105        request.resolve(value).expect("failed to resolve request");
106        let event = self.context.updates().expect_one_event();
107        self.update(event, model)
108    }
109
110    /// Run the app's `view` function with a model state
111    pub fn view(&self, model: &App::Model) -> App::ViewModel {
112        self.app.view(model)
113    }
114}
115
116impl<App> Default for AppTester<App>
117where
118    App: crate::App,
119    App::Capabilities: WithContext<App::Event, App::Effect>,
120{
121    fn default() -> Self {
122        let (command_sender, commands) = crate::capability::channel();
123        let (event_sender, events) = crate::capability::channel();
124        let (executor, spawner) = executor_and_spawner();
125        let capability_context = ProtoContext::new(command_sender, event_sender, spawner);
126        let command_spawner = CommandSpawner::new(capability_context.clone());
127
128        Self {
129            app: App::default(),
130            capabilities: App::Capabilities::new_with_context(capability_context),
131            context: Arc::new(AppContext {
132                commands,
133                events,
134                executor,
135            }),
136            command_spawner,
137        }
138    }
139}
140
141impl<App> AsRef<App::Capabilities> for AppTester<App>
142where
143    App: crate::App,
144{
145    fn as_ref(&self) -> &App::Capabilities {
146        &self.capabilities
147    }
148}
149
150impl<Ef, Ev> AppContext<Ef, Ev> {
151    pub fn updates(self: &Arc<Self>) -> Update<Ef, Ev> {
152        self.executor.run_all();
153        let effects = self.commands.drain().collect();
154        let events = self.events.drain().collect();
155
156        Update { effects, events }
157    }
158}
159
160/// Update test helper holds the result of running an app update using [`AppTester::update`]
161/// or resolving a request with [`AppTester::resolve`].
162#[derive(Debug)]
163#[must_use]
164pub struct Update<Ef, Ev> {
165    /// Effects requested from the update run
166    pub effects: Vec<Ef>,
167    /// Events dispatched from the update run
168    pub events: Vec<Ev>,
169}
170
171impl<Ef, Ev> Update<Ef, Ev> {
172    pub fn into_effects(self) -> impl Iterator<Item = Ef> {
173        self.effects.into_iter()
174    }
175
176    pub fn effects(&self) -> impl Iterator<Item = &Ef> {
177        self.effects.iter()
178    }
179
180    pub fn effects_mut(&mut self) -> impl Iterator<Item = &mut Ef> {
181        self.effects.iter_mut()
182    }
183
184    /// Assert that the update contains exactly one effect and zero events,
185    /// and return the effect
186    #[track_caller]
187    pub fn expect_one_effect(mut self) -> Ef {
188        if self.events.is_empty() && self.effects.len() == 1 {
189            self.effects.pop().unwrap()
190        } else {
191            panic!(
192                "Expected one effect but found {} effect(s) and {} event(s)",
193                self.effects.len(),
194                self.events.len()
195            );
196        }
197    }
198
199    /// Assert that the update contains exactly one event and zero effects,
200    /// and return the event
201    #[track_caller]
202    pub fn expect_one_event(mut self) -> Ev {
203        if self.effects.is_empty() && self.events.len() == 1 {
204            self.events.pop().unwrap()
205        } else {
206            panic!(
207                "Expected one event but found {} effect(s) and {} event(s)",
208                self.effects.len(),
209                self.events.len()
210            );
211        }
212    }
213
214    /// Assert that the update contains no effects or events
215    #[track_caller]
216    pub fn assert_empty(self) {
217        if self.effects.is_empty() && self.events.is_empty() {
218            return;
219        }
220        panic!(
221            "Expected empty update but found {} effect(s) and {} event(s)",
222            self.effects.len(),
223            self.events.len()
224        );
225    }
226
227    /// Take effects matching the `predicate` out of the [`Update`]
228    /// and return them, mutating the `Update`
229    pub fn take_effects<P>(&mut self, predicate: P) -> VecDeque<Ef>
230    where
231        P: FnMut(&Ef) -> bool,
232    {
233        let (matching_effects, other_effects) = self.take_effects_partitioned_by(predicate);
234
235        self.effects = other_effects.into_iter().collect();
236
237        matching_effects
238    }
239
240    /// Take all of the effects out of the [`Update`]
241    /// and split them into those matching `predicate` and the rest
242    pub fn take_effects_partitioned_by<P>(&mut self, predicate: P) -> (VecDeque<Ef>, VecDeque<Ef>)
243    where
244        P: FnMut(&Ef) -> bool,
245    {
246        std::mem::take(&mut self.effects)
247            .into_iter()
248            .partition(predicate)
249    }
250}
251
252impl<Effect, Event> Command<Effect, Event>
253where
254    Effect: Send + 'static,
255    Event: Send + 'static,
256{
257    /// Assert that the Command contains _exactly_ one effect and zero events,
258    /// and return the effect
259    #[track_caller]
260    pub fn expect_one_effect(&mut self) -> Effect {
261        if self.events().next().is_some() {
262            panic!("Expected only one effect, but found an event");
263        }
264        if let Some(effect) = self.effects().next() {
265            effect
266        } else {
267            panic!("Expected one effect but found none");
268        }
269    }
270}
271
272/// Panics if the pattern doesn't match an `Effect` from the specified `Update`
273///
274/// Like in a `match` expression, the pattern can be optionally followed by `if`
275/// and a guard expression that has access to names bound by the pattern.
276///
277/// # Example
278///
279/// ```
280/// # use crux_core::testing::Update;
281/// # enum Effect { Render(String) };
282/// # enum Event { None };
283/// # let effects = vec![Effect::Render("test".to_string())].into_iter().collect();
284/// # let mut update = Update { effects, events: vec!(Event::None) };
285/// use crux_core::assert_effect;
286/// assert_effect!(update, Effect::Render(_));
287/// ```
288#[macro_export]
289macro_rules! assert_effect {
290    ($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )? $(,)?) => {
291        assert!($expression.effects().any(|e| matches!(e, $( $pattern )|+ $( if $guard )?)));
292    };
293}