Skip to main content

test_better_matchers/
subject.rs

1//! The [`expect!`](crate::expect) macro and its [`Subject`] type: the entry point for writing
2//! an assertion.
3//!
4//! `expect!(value)` captures the value *and the source text of the expression
5//! it came from*, so a failure can name `2 + 2`, not just `4`. The resulting
6//! [`Subject`] is consumed by [`Subject::to`] / [`Subject::to_not`], each of
7//! which returns a [`TestResult`] so the assertion chains with `?`.
8//!
9//! # Async
10//!
11//! When the expression handed to `expect!` is a [`Future`], the resulting
12//! `Subject` grows an `await`-based method, [`Subject::resolves_to`]. The
13//! design is a single `Subject<T>` with that method added to *this* impl block
14//! under a method-level `where T: Future` bound and a distinct name: a blanket
15//! `impl<T> Subject<T>` and an overlapping `impl<F: Future> Subject<F>` cannot
16//! coexist as inherent impls.
17//!
18//! `resolves_to` is runtime-agnostic: it just awaits the future, so it works
19//! under `#[tokio::test]`, `#[async_std::test]`, `pollster::block_on`, or any
20//! other executor.
21
22use std::fmt::Display;
23use std::future::Future;
24use std::panic::Location;
25use std::time::Duration;
26
27use test_better_async::{Elapsed, RuntimeAvailable, run_within};
28use test_better_core::{ErrorKind, Payload, TestError, TestResult};
29use test_better_snapshot::{
30    InlineLocation, InlineSnapshotFailure, Redactions, SnapshotFailure, SnapshotMode,
31};
32
33use crate::description::Description;
34use crate::matcher::{Matcher, Mismatch};
35
36/// A value under test, paired with the source text of the expression that
37/// produced it.
38///
39/// `Subject` owns its value (the [`expect!`](crate::expect) macro hands it over by value) and
40/// borrows nothing, so it carries no lifetime parameter.
41pub struct Subject<T> {
42    actual: T,
43    expr: &'static str,
44    module_path: &'static str,
45}
46
47impl<T> Subject<T> {
48    /// Pairs `actual` with the source text it came from and the `module_path!()`
49    /// of the call site. Called by [`expect!`](crate::expect); rarely
50    /// constructed directly.
51    ///
52    /// `module_path` is only consulted by [`to_match_snapshot`](Self::to_match_snapshot),
53    /// which uses it to name the snapshot file; every other method ignores it.
54    #[must_use]
55    pub fn new(actual: T, expr: &'static str, module_path: &'static str) -> Self {
56        Self {
57            actual,
58            expr,
59            module_path,
60        }
61    }
62
63    /// Asserts that the value satisfies `matcher`.
64    ///
65    /// Returns `Ok(())` on a match and a [`TestError`] otherwise. The result is
66    /// `#[must_use]` (it is a `Result`), so a forgotten `?` is a compiler
67    /// warning rather than a silently-passing assertion.
68    #[track_caller]
69    pub fn to<M>(self, matcher: M) -> TestResult
70    where
71        M: Matcher<T>,
72    {
73        match matcher.check(&self.actual).failure {
74            None => Ok(()),
75            Some(mismatch) => Err(mismatch_error(self.expr, mismatch)),
76        }
77    }
78
79    /// Asserts that the value does *not* satisfy `matcher`.
80    ///
81    /// Returns `Ok(())` when the matcher does not match, and a [`TestError`]
82    /// when it unexpectedly does.
83    #[track_caller]
84    pub fn to_not<M>(self, matcher: M) -> TestResult
85    where
86        M: Matcher<T>,
87    {
88        if matcher.check(&self.actual).matched {
89            Err(unexpected_match_error(self.expr, matcher.description()))
90        } else {
91            Ok(())
92        }
93    }
94
95    /// Awaits the future and asserts that its output satisfies `matcher`.
96    ///
97    /// This is the async counterpart of [`to`](Self::to): reach for it when
98    /// the expression handed to [`expect!`](crate::expect) is a [`Future`].
99    /// The matcher runs against the future's *output*, so
100    /// `expect!(fut).resolves_to(eq(4))` is exactly `expect!(fut.await).to(eq(4))`
101    /// without the intermediate binding.
102    ///
103    /// The method itself is *not* `async`: it is `#[track_caller]` and returns
104    /// a future. The call-site location is captured synchronously when
105    /// `resolves_to` is called (an `async fn` could not be `#[track_caller]`),
106    /// then carried into the failure once the returned future is awaited.
107    ///
108    /// ```
109    /// use test_better_core::TestResult;
110    /// use test_better_matchers::{eq, expect};
111    ///
112    /// # fn main() -> TestResult {
113    /// pollster::block_on(async {
114    ///     expect!(async { 2 + 2 }).resolves_to(eq(4)).await?;
115    ///     TestResult::Ok(())
116    /// })
117    /// # }
118    /// ```
119    #[track_caller]
120    pub fn resolves_to<M>(self, matcher: M) -> impl Future<Output = TestResult>
121    where
122        T: Future,
123        M: Matcher<T::Output>,
124    {
125        // Captured here, synchronously, before the returned future is ever
126        // polled: this is the user's `expect!(..).resolves_to(..)` call site.
127        let location = Location::caller();
128        async move {
129            let output = self.actual.await;
130            match matcher.check(&output).failure {
131                None => Ok(()),
132                Some(mismatch) => Err(mismatch_error(self.expr, mismatch).with_location(location)),
133            }
134        }
135    }
136
137    /// Awaits the future, but fails if it does not finish within `limit`.
138    ///
139    /// Like [`resolves_to`](Self::resolves_to), this is for a future-typed
140    /// subject and returns a future rather than being `async` itself, so the
141    /// `#[track_caller]` location is the call site. Unlike `resolves_to`, it
142    /// does not look at the output: the assertion is purely about *time*.
143    ///
144    /// The timeout needs a runtime to provide its sleep, selected by a cargo
145    /// feature on `test-better`: `tokio`, `async-std`, or `smol`. With none
146    /// enabled, the `T: RuntimeAvailable` bound is unsatisfied and the call is
147    /// a compile error naming those flags.
148    ///
149    /// ```ignore
150    /// use std::time::Duration;
151    /// use test_better::prelude::*;
152    ///
153    /// # async fn run() -> TestResult {
154    /// expect!(some_future())
155    ///     .to_complete_within(Duration::from_millis(50))
156    ///     .await?;
157    /// # Ok(())
158    /// # }
159    /// ```
160    #[track_caller]
161    pub fn to_complete_within(self, limit: Duration) -> impl Future<Output = TestResult>
162    where
163        T: Future + RuntimeAvailable,
164    {
165        let location = Location::caller();
166        async move {
167            match run_within(limit, self.actual).await {
168                Ok(_) => Ok(()),
169                Err(elapsed) => Err(timeout_error(self.expr, elapsed).with_location(location)),
170            }
171        }
172    }
173
174    /// Asserts that the value matches the snapshot stored under `name`.
175    ///
176    /// The snapshot lives at `tests/snapshots/<module-path>__<name>.snap` in the
177    /// package under test, with `<module-path>` taken from the call site's
178    /// `module_path!()`. On a normal run the value is compared against that
179    /// file; a difference (or a missing file) is a `Snapshot` failure, and the
180    /// difference is rendered with the standard line-oriented diff. Rerun the
181    /// test with `UPDATE_SNAPSHOTS=1` to create the file the first time, or to
182    /// accept an intentional change.
183    ///
184    /// The value only has to be [`Display`]: that is what gets written to and
185    /// compared against the file.
186    ///
187    /// ```no_run
188    /// use test_better_core::TestResult;
189    /// use test_better_matchers::expect;
190    ///
191    /// # fn main() -> TestResult {
192    /// // Run once with `UPDATE_SNAPSHOTS=1` to record the snapshot; later runs
193    /// // compare against `tests/snapshots/<module>__homepage.snap`.
194    /// expect!("<h1>Hello</h1>").to_match_snapshot("homepage")?;
195    /// # Ok(())
196    /// # }
197    /// ```
198    #[track_caller]
199    pub fn to_match_snapshot(self, name: &str) -> TestResult
200    where
201        T: Display,
202    {
203        self.to_match_snapshot_with(name, &Redactions::new())
204    }
205
206    /// Like [`to_match_snapshot`](Self::to_match_snapshot), but runs
207    /// `redactions` over the value first.
208    ///
209    /// Use this when the rendered value carries content that is not stable run
210    /// to run (a generated UUID, a timestamp): the redactions rewrite those
211    /// spans to fixed placeholders, so the snapshot file stays meaningful.
212    /// Because the redactions run on every comparison, the placeholder is what
213    /// is stored and what later runs compare against.
214    ///
215    /// ```no_run
216    /// use test_better_core::TestResult;
217    /// use test_better_matchers::expect;
218    /// use test_better_snapshot::Redactions;
219    ///
220    /// # fn main() -> TestResult {
221    /// let rendered = format!("created {}", uuid_of_new_record());
222    /// expect!(rendered)
223    ///     .to_match_snapshot_with("record", &Redactions::new().redact_uuids())?;
224    /// # Ok(())
225    /// # }
226    /// # fn uuid_of_new_record() -> &'static str { "00000000-0000-0000-0000-000000000000" }
227    /// ```
228    #[track_caller]
229    pub fn to_match_snapshot_with(self, name: &str, redactions: &Redactions) -> TestResult
230    where
231        T: Display,
232    {
233        let actual = redactions.apply(&self.actual.to_string());
234        match test_better_snapshot::assert_snapshot(self.module_path, name, &actual) {
235            Ok(()) => Ok(()),
236            Err(failure) => Err(snapshot_error(self.expr, name, failure)),
237        }
238    }
239
240    /// Asserts that the value matches the inline snapshot literal `expected`.
241    ///
242    /// Unlike [`to_match_snapshot`](Self::to_match_snapshot), the snapshot lives
243    /// in the test source: `expected` *is* the snapshot. The literal is
244    /// normalized before comparison (a leading newline and the common
245    /// indentation are cosmetic), so it can be indented to match the
246    /// surrounding code.
247    ///
248    /// On a mismatch with `UPDATE_SNAPSHOTS` unset this fails like any
249    /// assertion. With `UPDATE_SNAPSHOTS=1` it records a *pending patch* under
250    /// `target/test-better-pending/` and passes; the `test-better-accept`
251    /// companion binary rewrites the literal in the source from those patches.
252    /// A proc macro could not do this rewrite (it runs before the test), so the
253    /// work is split: this method captures the call-site location with
254    /// `#[track_caller]`, the accept binary edits the file.
255    ///
256    /// ```no_run
257    /// use test_better_core::TestResult;
258    /// use test_better_matchers::expect;
259    ///
260    /// # fn main() -> TestResult {
261    /// expect!(2 + 2).to_match_inline_snapshot("4")?;
262    /// # Ok(())
263    /// # }
264    /// ```
265    #[track_caller]
266    pub fn to_match_inline_snapshot(self, expected: &str) -> TestResult
267    where
268        T: Display,
269    {
270        self.to_match_inline_snapshot_with(expected, &Redactions::new())
271    }
272
273    /// Like [`to_match_inline_snapshot`](Self::to_match_inline_snapshot), but
274    /// runs `redactions` over the value first.
275    ///
276    /// The inline-snapshot counterpart of
277    /// [`to_match_snapshot_with`](Self::to_match_snapshot_with): redaction
278    /// rewrites non-deterministic spans to fixed placeholders before the
279    /// comparison, so `UPDATE_SNAPSHOTS=1` records the *redacted* value as the
280    /// literal and later runs stay green.
281    ///
282    /// ```no_run
283    /// use test_better_core::TestResult;
284    /// use test_better_matchers::expect;
285    /// use test_better_snapshot::Redactions;
286    ///
287    /// # fn main() -> TestResult {
288    /// let rendered = format!("at {}", now_rfc3339());
289    /// expect!(rendered).to_match_inline_snapshot_with(
290    ///     "at [timestamp]",
291    ///     &Redactions::new().redact_rfc3339_timestamps(),
292    /// )?;
293    /// # Ok(())
294    /// # }
295    /// # fn now_rfc3339() -> &'static str { "2026-05-14T12:34:56Z" }
296    /// ```
297    #[track_caller]
298    pub fn to_match_inline_snapshot_with(
299        self,
300        expected: &str,
301        redactions: &Redactions,
302    ) -> TestResult
303    where
304        T: Display,
305    {
306        let actual = redactions.apply(&self.actual.to_string());
307        let caller = Location::caller();
308        let location = InlineLocation {
309            file: caller.file().to_string(),
310            line: caller.line(),
311            column: caller.column(),
312        };
313        match test_better_snapshot::assert_inline_snapshot(
314            &actual,
315            expected,
316            &location,
317            SnapshotMode::from_env(),
318        ) {
319            Ok(()) => Ok(()),
320            Err(failure) => Err(inline_snapshot_error(self.expr, failure)),
321        }
322    }
323}
324
325/// Builds the error for a matcher that did not match: the expected/actual pair
326/// goes into the payload, the source expression into the message.
327#[track_caller]
328fn mismatch_error(expr: &str, mismatch: Mismatch) -> TestError {
329    TestError::new(ErrorKind::Assertion)
330        .with_message(format!("expect!({expr})"))
331        .with_payload(Payload::ExpectedActual {
332            expected: mismatch.expected.to_string(),
333            actual: mismatch.actual,
334            diff: mismatch.diff,
335        })
336}
337
338/// Builds the error for `to_not` when the matcher matched but should not have.
339/// There is no `Mismatch` in this case, so the message carries the whole story.
340#[track_caller]
341fn unexpected_match_error(expr: &str, description: Description) -> TestError {
342    TestError::new(ErrorKind::Assertion).with_message(format!(
343        "expect!({expr}): expected it not to be {description}, but it was"
344    ))
345}
346
347/// Builds the error for `to_complete_within` when the future ran past its
348/// limit. This is a timing failure, not a value mismatch, so it carries only
349/// a message, no payload.
350#[track_caller]
351fn timeout_error(expr: &str, elapsed: Elapsed) -> TestError {
352    TestError::new(ErrorKind::Assertion).with_message(format!(
353        "expect!({expr}): did not complete within {:?}",
354        elapsed.limit
355    ))
356}
357
358/// Renders a [`SnapshotFailure`] from `test-better-snapshot` into a `TestError`.
359///
360/// A mismatch becomes an `ExpectedActual` payload, exactly like a value
361/// matcher's mismatch, so the standard renderer shows it (and its diff) the
362/// same way. A missing file or an I/O error has no two sides to compare, so it
363/// carries only a message.
364#[track_caller]
365fn snapshot_error(expr: &str, name: &str, failure: SnapshotFailure) -> TestError {
366    match failure {
367        SnapshotFailure::Mismatch {
368            path,
369            expected,
370            actual,
371        } => {
372            let diff = snapshot_diff(&expected, &actual);
373            TestError::new(ErrorKind::Snapshot)
374                .with_message(format!(
375                    "expect!({expr}): snapshot {name:?} at {} does not match",
376                    path.display()
377                ))
378                .with_payload(Payload::ExpectedActual {
379                    expected,
380                    actual,
381                    diff,
382                })
383        }
384        SnapshotFailure::Missing { path } => {
385            TestError::new(ErrorKind::Snapshot).with_message(format!(
386                "expect!({expr}): snapshot {name:?} does not exist at {}; \
387                 rerun with UPDATE_SNAPSHOTS=1 to create it",
388                path.display()
389            ))
390        }
391        SnapshotFailure::Io {
392            path,
393            action,
394            source,
395        } => TestError::new(ErrorKind::Snapshot).with_message(format!(
396            "expect!({expr}): snapshot {name:?} I/O error {action} ({}): {source}",
397            path.display()
398        )),
399    }
400}
401
402/// Renders an [`InlineSnapshotFailure`] into a `TestError`, the inline-snapshot
403/// counterpart of [`snapshot_error`]. Both sides go into an `ExpectedActual`
404/// payload so the standard renderer (and its diff) show the change.
405#[track_caller]
406fn inline_snapshot_error(expr: &str, failure: InlineSnapshotFailure) -> TestError {
407    let InlineSnapshotFailure { expected, actual } = failure;
408    let diff = snapshot_diff(&expected, &actual);
409    TestError::new(ErrorKind::Snapshot)
410        .with_message(format!(
411            "expect!({expr}): inline snapshot does not match; \
412             rerun with UPDATE_SNAPSHOTS=1 to update it"
413        ))
414        .with_payload(Payload::ExpectedActual {
415            expected,
416            actual,
417            diff,
418        })
419}
420
421/// The line-oriented diff for a snapshot mismatch. Unlike a value matcher,
422/// which only diffs multi-line values, a snapshot is text and a diff is always
423/// the readable way to show what changed. With the `diff` feature off this is
424/// `None` and the failure still renders, just without the diff.
425#[cfg(feature = "diff")]
426fn snapshot_diff(expected: &str, actual: &str) -> Option<String> {
427    Some(crate::diff::diff_lines(expected, actual))
428}
429
430#[cfg(not(feature = "diff"))]
431fn snapshot_diff(_expected: &str, _actual: &str) -> Option<String> {
432    None
433}
434
435/// Captures an expression and its source text for assertion with a matcher.
436///
437/// ```
438/// use test_better_core::TestResult;
439/// use test_better_matchers::{eq, expect};
440///
441/// fn main() -> TestResult {
442///     expect!(2 + 2).to(eq(4))?;
443///     Ok(())
444/// }
445/// ```
446#[macro_export]
447macro_rules! expect {
448    ($actual:expr) => {
449        $crate::Subject::new($actual, ::core::stringify!($actual), ::core::module_path!())
450    };
451}
452
453#[cfg(test)]
454mod tests {
455    use test_better_core::TestResult;
456
457    use crate::{eq, is_true};
458
459    #[test]
460    fn to_returns_ok_on_a_match() -> TestResult {
461        let result = expect!(2 + 2).to(eq(4));
462        expect!(result.is_ok()).to(is_true())?;
463        Ok(())
464    }
465
466    #[test]
467    fn to_failure_mentions_the_expression_and_the_expected_value() -> TestResult {
468        let error = expect!(2 + 2).to(eq(5)).expect_err("2 + 2 is not 5");
469        let rendered = error.to_string();
470        expect!(rendered.contains("2 + 2")).to(is_true())?;
471        expect!(rendered.contains("equal to 5")).to(is_true())?;
472        expect!(rendered.contains("actual: 4")).to(is_true())?;
473        Ok(())
474    }
475
476    #[test]
477    fn to_failure_captures_the_caller_location() -> TestResult {
478        let line = line!() + 1;
479        let error = expect!(2 + 2).to(eq(5)).expect_err("2 + 2 is not 5");
480        expect!(error.location.line()).to(eq(line))?;
481        expect!(error.location.file().ends_with("subject.rs")).to(is_true())?;
482        Ok(())
483    }
484
485    #[test]
486    fn to_not_returns_ok_when_the_matcher_does_not_match() -> TestResult {
487        let result = expect!(2 + 2).to_not(eq(5));
488        expect!(result.is_ok()).to(is_true())?;
489        Ok(())
490    }
491
492    #[test]
493    fn to_not_failure_mentions_the_expression_and_the_matcher() -> TestResult {
494        let error = expect!(true).to_not(is_true()).expect_err("true is true");
495        let rendered = error.to_string();
496        expect!(rendered.contains("expect!(true)")).to(is_true())?;
497        expect!(rendered.contains("not to be true")).to(is_true())?;
498        Ok(())
499    }
500
501    #[test]
502    fn to_not_captures_the_caller_location() -> TestResult {
503        let line = line!() + 1;
504        let error = expect!(true).to_not(is_true()).expect_err("true is true");
505        expect!(error.location.line()).to(eq(line))?;
506        Ok(())
507    }
508
509    #[test]
510    fn resolves_to_returns_ok_when_the_output_matches() -> TestResult {
511        pollster::block_on(async {
512            let result = expect!(async { 2 + 2 }).resolves_to(eq(4)).await;
513            expect!(result.is_ok()).to(is_true())
514        })
515    }
516
517    #[test]
518    fn resolves_to_failure_mentions_the_expression_and_the_output() -> TestResult {
519        pollster::block_on(async {
520            let error = expect!(async { 2 + 2 })
521                .resolves_to(eq(5))
522                .await
523                .expect_err("2 + 2 does not resolve to 5");
524            let rendered = error.to_string();
525            expect!(rendered.contains("async { 2 + 2 }")).to(is_true())?;
526            expect!(rendered.contains("equal to 5")).to(is_true())?;
527            expect!(rendered.contains("actual: 4")).to(is_true())
528        })
529    }
530
531    #[test]
532    fn resolves_to_failure_captures_the_call_site_not_the_await() -> TestResult {
533        // The location is captured where `resolves_to` is *called*, even
534        // though the future is awaited on a later line.
535        pollster::block_on(async {
536            let line = line!() + 1;
537            let pending = expect!(async { 2 + 2 }).resolves_to(eq(5));
538            let error = pending.await.expect_err("2 + 2 does not resolve to 5");
539            expect!(error.location.line()).to(eq(line))?;
540            expect!(error.location.file().ends_with("subject.rs")).to(is_true())
541        })
542    }
543
544    #[test]
545    fn snapshot_mismatch_renders_as_a_snapshot_error_with_a_diff() -> TestResult {
546        use std::path::PathBuf;
547
548        use test_better_core::ErrorKind;
549        use test_better_snapshot::SnapshotFailure;
550
551        let failure = SnapshotFailure::Mismatch {
552            path: PathBuf::from("tests/snapshots/m__page.snap"),
553            expected: "line one\nline two".to_string(),
554            actual: "line one\nline TWO".to_string(),
555        };
556        let error = super::snapshot_error("page", "page", failure);
557        expect!(error.kind == ErrorKind::Snapshot).to(is_true())?;
558        let rendered = error.to_string();
559        expect!(rendered.contains("snapshot \"page\"")).to(is_true())?;
560        // The expected/actual payload renders, and `diff` is on by default.
561        expect!(rendered.contains("line one")).to(is_true())?;
562        #[cfg(feature = "diff")]
563        expect!(rendered.contains("-line two")).to(is_true())?;
564        Ok(())
565    }
566
567    #[test]
568    fn snapshot_missing_renders_as_a_snapshot_error_pointing_at_update() -> TestResult {
569        use std::path::PathBuf;
570
571        use test_better_core::ErrorKind;
572        use test_better_snapshot::SnapshotFailure;
573
574        let failure = SnapshotFailure::Missing {
575            path: PathBuf::from("tests/snapshots/m__page.snap"),
576        };
577        let error = super::snapshot_error("page", "page", failure);
578        expect!(error.kind == ErrorKind::Snapshot).to(is_true())?;
579        expect!(error.to_string().contains("UPDATE_SNAPSHOTS=1")).to(is_true())?;
580        Ok(())
581    }
582
583    #[test]
584    fn inline_snapshot_mismatch_renders_as_a_snapshot_error_with_a_diff() -> TestResult {
585        use test_better_core::ErrorKind;
586        use test_better_snapshot::InlineSnapshotFailure;
587
588        let failure = InlineSnapshotFailure {
589            expected: "one\ntwo".to_string(),
590            actual: "one\nTWO".to_string(),
591        };
592        let error = super::inline_snapshot_error("value", failure);
593        expect!(error.kind == ErrorKind::Snapshot).to(is_true())?;
594        let rendered = error.to_string();
595        expect!(rendered.contains("inline snapshot does not match")).to(is_true())?;
596        expect!(rendered.contains("UPDATE_SNAPSHOTS=1")).to(is_true())?;
597        #[cfg(feature = "diff")]
598        expect!(rendered.contains("-two")).to(is_true())?;
599        Ok(())
600    }
601}