Skip to main content

test_better_matchers/
subject.rs

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