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*/