xpct/docs/
writing_matchers.rs

1/*!
2# Writing Custom Matchers
3
4How to write custom matchers for your tests.
5
6[↩︎ Back to User Docs](crate::docs)
7
8If none of the provided matchers suit your needs, xpct allows you to write
9custom matchers. There are a few ways to do this. In increasing order of
10complexity and flexibility, you can:
11
121. Compose existing matchers. This is the simplest approach, but doesn't let you
13   customize the formatting of the failure output.
142. Implement [`Match`]. This lets you customize the formatting of the failure
15   output.
163. Implement [`TransformMatch`]. This is like [`Match`], but additionally allows
17   you to write matchers that transform values (like the [`be_some`] and
18   [`be_ok`] matchers do).
19
20## Composing existing matchers
21
22The simplest way to make custom matchers is to just compose existing matchers.
23The combinator matchers [`each`], [`any`], and [`all`] are useful for this.
24
25```
26use std::fmt;
27use xpct::{each, be_lt, be_gt};
28use xpct::core::Matcher;
29
30pub fn be_between<'a, Actual, Low, High>(
31    low: &'a Low,
32    high: &'a High,
33) -> Matcher<'a, Actual, Actual>
34where
35    Actual: PartialOrd<Low> + PartialOrd<High> + fmt::Debug + 'a,
36    Low: fmt::Debug,
37    High: fmt::Debug,
38{
39    each(move |ctx| {
40        ctx.borrow().to(be_gt(low)).to(be_lt(high));
41    })
42}
43```
44
45## Implementing `Match`
46
47The next simplest way is to implement the [`Match`] trait. This is how many of
48the provided matchers are implemented. Here's an implementation of the [`equal`]
49matcher.
50
51```
52use xpct::core::Match;
53use xpct::matchers::Mismatch;
54
55pub struct EqualMatcher<Expected> {
56    expected: Expected,
57}
58
59impl<Expected> EqualMatcher<Expected> {
60    pub fn new(expected: Expected) -> Self {
61        Self { expected }
62    }
63}
64
65impl<Expected, Actual> Match<Actual> for EqualMatcher<Expected>
66where
67    Actual: PartialEq<Expected> + Eq,
68{
69    type Fail = Mismatch<Expected, Actual>;
70
71    fn matches(&mut self, actual: &Actual) -> xpct::Result<bool> {
72        Ok(actual == &self.expected)
73    }
74
75    fn fail(self, actual: Actual) -> Self::Fail {
76        Mismatch {
77            actual,
78            expected: self.expected,
79        }
80    }
81}
82
83```
84
85Now let's make a function to call this matcher ergonomically from tests!
86Basically, we just need to write a function which returns a [`Matcher`].
87
88To make `EqualMatcher` into a `Matcher`, you just need to wrap it with
89[`Matcher::new`]. This method also accepts the formatter which is used to format
90the output. Thankfully, you don't need to write the formatting logic yourself to
91get pretty output! Because our matcher returns a [`Mismatch`] when it fails, we
92can use any formatter which accepts a [`Mismatch`], like the provided
93[`MismatchFormat`].
94
95```
96# use xpct::matchers::equal::EqualMatcher;
97use std::fmt;
98
99use xpct::expect;
100use xpct::core::Matcher;
101use xpct::format::MismatchFormat;
102
103pub fn equal<'a, Actual, Expected>(expected: Expected) -> Matcher<'a, Actual, Actual>
104where
105    Actual: fmt::Debug + PartialEq<Expected> + Eq + 'a,
106    Expected: fmt::Debug + 'a,
107{
108    Matcher::new(
109        EqualMatcher::new(expected),
110        MismatchFormat::new("to equal", "to not equal"),
111    )
112}
113
114```
115
116What if we wanted to make a matcher which is the negated version of
117`EqualMatcher`, like `not_equal`? For a matcher created by implementing
118[`Match`], we can call [`Matcher::neg`] to negate it.
119
120```
121# use xpct::matchers::equal::EqualMatcher;
122use std::fmt;
123
124use xpct::expect;
125use xpct::core::Matcher;
126use xpct::format::MismatchFormat;
127
128pub fn not_equal<'a, Actual, Expected>(expected: Expected) -> Matcher<'a, Actual, Actual>
129where
130    Actual: fmt::Debug + PartialEq<Expected> + Eq + 'a,
131    Expected: fmt::Debug + 'a,
132{
133    Matcher::neg(
134        EqualMatcher::new(expected),
135        // Remember that we need to flip these cases, because `actual !=
136        // expected` is now the *positive* case and `actual == expected` is now
137        // the *negative* case.
138        MismatchFormat::new("to not equal", "to equal"),
139    )
140}
141
142expect!("disco").to(not_equal("not disco"));
143```
144
145## Implementing `TransformMatch`
146
147The major limitation of [`Match`] is that it always returns the same value that
148was passed in. If you need it to transform the value like the [`be_some`] and
149[`be_ok`] matchers do, you can implement the [`TransformMatch`] trait.
150
151```
152use std::marker::PhantomData;
153
154use xpct::core::{Matcher, TransformMatch, MatchOutcome};
155use xpct::matchers::Expectation;
156
157pub struct BeOkMatcher<T, E> {
158    // Matchers created by implementing `TransformMatch` will often need to use
159    // `PhantomData` so they know their input and output types.
160    marker: PhantomData<(T, E)>,
161}
162
163impl<T, E> BeOkMatcher<T, E> {
164    pub fn new() -> Self {
165        Self {
166            marker: PhantomData,
167        }
168    }
169}
170
171impl<T, E> TransformMatch for BeOkMatcher<T, E> {
172    // The type the matcher accepts.
173    type In = Result<T, E>;
174
175    // In the positive case, this should return the `Ok` value.
176    type PosOut = T;
177
178    // In the negative case, this should return the `Err` value.
179    type NegOut = E;
180
181    // We use the `Expectation` type here to include the actual value in the
182    // failure output.
183    type PosFail = Expectation<Result<T, E>>;
184    type NegFail = Expectation<Result<T, E>>;
185
186    fn match_pos(
187        self,
188        actual: Self::In,
189    ) -> xpct::Result<MatchOutcome<Self::PosOut, Self::PosFail>> {
190        match actual {
191            Ok(value) => Ok(MatchOutcome::Success(value)),
192            Err(err) => Ok(MatchOutcome::Fail(Expectation { actual: Err(err) })),
193        }
194    }
195
196    fn match_neg(
197        self,
198        actual: Self::In,
199    ) -> xpct::Result<MatchOutcome<Self::NegOut, Self::NegFail>> {
200        match actual {
201            Ok(value) => Ok(MatchOutcome::Fail(Expectation { actual: Ok(value) })),
202            Err(error) => Ok(MatchOutcome::Success(error)),
203        }
204    }
205}
206```
207
208You'll see the terms "pos" and "neg", short for *positive* and *negative*,
209throughout the API. These refer to whether a matcher is negated (negative)
210or not negated (positive).
211
212If a matcher is negated (the negative case), it means that we're expecting it to
213fail. If a matcher is *not* negated (the positive case), it means we're
214expecting it to succeed.
215
216Now let's make some functions for invoking our matcher.
217
218```
219use std::fmt;
220
221# use xpct::matchers::result::BeOkMatcher;
222use xpct::core::{Matcher, NegFormat};
223use xpct::format::ExpectationFormat;
224
225// `ExpectationFormat` is a simple formatter that just returns the actual value
226// and a static message.
227fn result_format<T>() -> ExpectationFormat<T> {
228    ExpectationFormat::new("to be Ok(_)", "to be Err(_)")
229}
230
231pub fn be_ok<'a, T, E>() -> Matcher<'a, Result<T, E>, T, E>
232where
233    T: fmt::Debug + 'a,
234    E: fmt::Debug + 'a,
235{
236    // For matchers implemented with `TransformMatch`, you use
237    // `Matcher::transform`.
238    Matcher::transform(BeOkMatcher::new(), result_format())
239}
240
241pub fn be_err<'a, T, E>() -> Matcher<'a, Result<T, E>, E, T>
242where
243    T: fmt::Debug + 'a,
244    E: fmt::Debug + 'a,
245{
246    // You can use `Matcher::transform_neg` to negate a matcher created by
247    // implementing `TransformMatch`. You can use `NegFormat` to negate the
248    // formatter.
249    Matcher::transform_neg(BeOkMatcher::new(), NegFormat(result_format()))
250}
251```
252
253[`TransformMatch`]: crate::core::TransformMatch
254[`each`]: crate::each
255[`any`]: crate::any
256[`all`]: crate::all
257[`be_some`]: crate::be_some
258[`be_ok`]: crate::be_ok
259[`Match`]: crate::core::Match
260[`equal`]: crate::equal
261[`Matcher`]: crate::core::Matcher
262[`Matcher::transform`]: crate::core::Matcher::transform
263[`Mismatch`]: crate::matchers::Mismatch
264[`MismatchFormat`]: crate::format::MismatchFormat
265[`Matcher::new`]: crate::core::Matcher::new
266[`Matcher::neg`]: crate::core::Matcher::neg
267*/