tested_fixture/
lib.rs

1//! Attribute macro for creating fixtures from tests
2//!
3//! ## Description
4//!
5//! Sometimes a series of tests are progressive or incremental; that is to say
6//! one test builds on another. A multi-stage test might have complicated
7//! setup and verification processes for each step, but with clear boundaries
8//! between stages (`test_1` verifies stage 1, `test_2` verifies stage 2, etc.
9//! ). The problem arises when stages want to share data (i.e. `test_2` wants
10//! to start where `test_1` left off).
11//!
12//! Common advice is to duplicate all the setup code across all tests, or
13//! alternatively to combine the tests into one large test. However the former
14//! approach can significantly slow down tests if setup is costly, and also
15//! introduces significant test maintenance costs if setup procedures change.
16//! The latter however can lead to large and unruly testing functions which are
17//! difficult to maintain, and doesn't solve the problem when dependencies
18//! cross multiple files (i.e. unit tests which test the full setup process for a
19//! `Foo` are difficult to combine with unit tests which test the setup process
20//! of a `Bar` which relies on a fully constructed `Foo`; should the "combined"
21//! test live near `Foo` or `Bar`? What if the tests needs to access internals to
22//! verify assertions?).
23//!
24//! This crate provides an alternative approach by allowing a test to return a
25//! fixture which can be used in subsequent tests. Tests can opt in to this
26//! functionality by using a single attribute macro [`tested_fixture`].
27//!
28//! ## Usage
29//!
30//! When writing tests for code like:
31//! ```
32//! struct Foo {
33//!     // ...
34//! }
35//!
36//! struct State {
37//!     // ...
38//! }
39//!
40//! impl Foo {
41//!     fn step_1() -> Self {
42//!         Foo {
43//!             // Complicated setup...
44//!         }
45//!     }
46//!
47//!     fn step_2(&self) -> State {
48//!         State {
49//!             // Complicated execution...
50//!         }
51//!     }
52//!
53//!     fn step_3(&self, v: &State) {
54//!         // Complicated execution...
55//!     }
56//! }
57//! ```
58//!
59//! An duplicated test setup would look something like
60//! ```
61//! #[test]
62//! fn step_1() {
63//!     let foo = Foo::step_1();
64//!     // Complicated assertions verify step 1...
65//! }
66//!
67//! #[test]
68//! fn step_2() {
69//!     let foo = Foo::step_1();
70//!     // (Some?) Complicated assertions verify step 1...
71//!
72//!     foo.step_2();
73//!     // Complicated assertions verify step 2...
74//! }
75//!
76//! #[test]
77//! fn step_3() {
78//!     let foo = Foo::step_1();
79//!     // (Some?) Complicated assertions verify step 1...
80//!
81//!     let state = foo.step_2();
82//!     // (Some?) Complicated assertions verify step 2...
83//!
84//!     foo.step_3(&state);
85//!     // Complicated assertions verify step 3...
86//! }
87//! ```
88//!
89//! As you can see, with a lot of steps, this can quickly get out of hand. To
90//! clean it up is straightforward by switching to use the
91//! `tested_fixture` attribute instead of the normal `test`.
92//!
93//! ```
94//! // Save the fixture in a static variable called `STEP_1`
95//! #[tested_fixture::tested_fixture(STEP_1)]
96//! fn step_1() -> Foo {
97//!     let foo = Foo::step_1();
98//!     // Complicated assertions verify step 1...
99//!     foo
100//! }
101//!
102//! #[tested_fixture::tested_fixture(STEP_2_STATE)]
103//! fn step_2() -> State {
104//!     let state = STEP_1.step_2();
105//!     // Complicated assertions verify step 2...
106//!     state
107//! }
108//!
109//! #[test]
110//! fn step_3() {
111//!     STEP_1.step_3(&STEP_2_STATE);
112//!     // Complicated assertions verify step 3...
113//! }
114//! ```
115//!
116//! Note that when only `step_2` is run, `STEP_1` will be initialized on
117//! first access. Since the order of tests is not guaranteed, this actually can
118//! occur even if both tests are run. But since results are cached, the
119//! `step_1` test should still succeed (or fail) regardless of if it is run
120//! first or not.
121//!
122//! ## Advanced usage
123//!
124//! The [`tested_fixture`] attribute supports attributes and a visibility level
125//! prefixing the identifier, as well as an optional `: type` suffix. This
126//! optional suffix can be used on tests returning a `Result` to specify that
127//! only `Ok` return values should be captured. For example:
128//!
129//! ```
130//! #[tested_fixture::tested_fixture(
131//!     /// Doc comment on the `STEP_1` global variable
132//!     pub(crate) STEP_1: Foo
133//! )]
134//! fn step_1() -> Result<Foo, &'static str> {
135//!     // ...
136//! }
137//! ```
138//!
139//! ## Limitations
140//!
141//! Ordinary `#[test]` functions are able to return anything which implements
142//! [`std::process::Termination`], including unlimited nestings of `Result`s.
143//! While this crate does support returning nested `Result` wrappings, it only
144//! does so up to a fixed depth. Additionally it does not support returning any
145//! other `Termination` implementations besides `Result`.
146//!
147//! As with all testing-related global state, it is recommended that tests don't
148//! mutate the state, as doing so will increase the risk of flaky tests due to
149//! changes in execution order or timing. Thankfully this is the default
150//! behavior, as all fixtures defined by this crate are only accessible by
151//! non-mutable reference.
152//!
153//! Right now this crate does not support async tests.
154
155#![warn(missing_docs)]
156#![allow(clippy::test_attr_in_doctest)]
157
158pub use tested_fixture_macros::tested_fixture;
159
160#[doc(hidden)]
161pub use tested_fixture_macros::tested_fixture_doctest;
162
163#[doc(hidden)]
164pub mod helpers {
165    use std::{
166        convert::Infallible,
167        fmt::Debug,
168        process::{ExitCode, Termination},
169    };
170
171    // Re-exports
172    pub use once_cell::sync::{Lazy, OnceCell};
173
174    /// A helper trait to unify `Result` fixtures types
175    pub trait MakeResultRef {
176        type Output;
177        fn make(self) -> Self::Output;
178    }
179
180    impl<T, E: Debug> MakeResultRef for &'static Result<T, E> {
181        type Output = Result<&'static T, &'static E>;
182        fn make(self) -> Self::Output {
183            self.as_ref()
184        }
185    }
186
187    /// A helper struct for wrapping fixtures
188    pub struct ReportSuccess<T>(pub T);
189
190    impl<T> Termination for ReportSuccess<T> {
191        fn report(self) -> ExitCode {
192            ExitCode::SUCCESS
193        }
194    }
195
196    /// Helper trait for unwrapping fixtures
197    pub trait StaticallyBorrow {
198        type T;
199        fn static_borrow(&self) -> Self::T;
200    }
201
202    impl<T> StaticallyBorrow for &'static T {
203        type T = &'static T;
204        fn static_borrow(&self) -> Self::T {
205            self
206        }
207    }
208
209    impl<T: StaticallyBorrow> StaticallyBorrow for Result<T, Infallible> {
210        type T = T::T;
211        fn static_borrow(&self) -> Self::T {
212            match self.as_ref() {
213                Ok(v) => v.static_borrow(),
214                Err(_) => unreachable!(),
215            }
216        }
217    }
218
219    impl<T: StaticallyBorrow> StaticallyBorrow for ReportSuccess<T> {
220        type T = T::T;
221        fn static_borrow(&self) -> Self::T {
222            self.0.static_borrow()
223        }
224    }
225
226    /// Helper trait for unwrapping fixtures
227    pub trait Unwrap<T>: Termination {
228        fn unwrap(self, context: &str) -> &'static T;
229    }
230
231    impl<T: 'static, R: StaticallyBorrow<T = &'static T>> Unwrap<T> for ReportSuccess<R> {
232        fn unwrap(self, _context: &str) -> &'static T {
233            self.static_borrow()
234        }
235    }
236
237    impl<T, R: Unwrap<T>, E: Debug> Unwrap<T> for Result<R, E> {
238        fn unwrap(self, context: &str) -> &'static T {
239            match self {
240                Ok(v) => v.unwrap(context),
241                Err(e) => panic!("{} failed: {:?}", context, e),
242            }
243        }
244    }
245
246    /// A helper struct to unify non-`Result` fixtures types
247    pub struct Fixer<T>(pub T);
248    impl<T: MakeResultRef> Fixer<T> {
249        pub fn fix(self) -> T::Output {
250            self.0.make()
251        }
252    }
253
254    /// A helper trait to unify non-`Result` fixtures types
255
256    pub trait Fix {
257        type Fixed;
258        fn fix(self) -> Self::Fixed;
259    }
260
261    impl<T: 'static> Fix for Fixer<T> {
262        type Fixed = Result<ReportSuccess<T>, Infallible>;
263        fn fix(self) -> Self::Fixed {
264            Ok(ReportSuccess(self.0))
265        }
266    }
267
268    /// A helper function to get fixtures from test functions
269    pub fn unwrap<T, R, F>(f: F) -> &'static T
270    where
271        T: 'static,
272        R: Unwrap<T>,
273        F: FnOnce() -> R,
274    {
275        let context = core::any::type_name::<F>();
276        f().unwrap(context)
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    struct HeavySetup(u32);
284
285    impl HeavySetup {
286        fn build(v: u32) -> Self {
287            HeavySetup(v)
288        }
289    }
290
291    #[tested_fixture(
292        /// This is a test fixture
293        pub(crate) SETUP_0
294    )]
295    fn setup_with_attributes() -> HeavySetup {
296        HeavySetup::build(1)
297    }
298
299    #[tested_fixture(SETUP_1)]
300    fn setup() -> HeavySetup {
301        HeavySetup::build(1)
302    }
303
304    #[tested_fixture(SETUP_2: HeavySetup)]
305    fn try_setup() -> Result<HeavySetup, &'static str> {
306        Ok(HeavySetup::build(2))
307    }
308
309    #[tested_fixture(SETUP_3: HeavySetup)]
310    #[ignore = "fails"]
311    fn fail_setup() -> Result<HeavySetup, &'static str> {
312        Err("failed due to reticulated splines")
313    }
314
315    #[tested_fixture(SETUP_4)]
316    #[ignore = "fails"]
317    fn panic_setup() -> HeavySetup {
318        panic!("failed due to normalized social network")
319    }
320
321    #[test]
322    fn combine_setup() {
323        let _ = HeavySetup::build(SETUP_1.0 + SETUP_2.0);
324    }
325
326    #[test]
327    #[should_panic(
328        expected = r#"tested_fixture::tests::fail_setup failed: "failed due to reticulated splines""#
329    )]
330    fn combine_fail() {
331        let _ = HeavySetup::build(SETUP_1.0 + SETUP_3.0);
332    }
333
334    #[test]
335    #[should_panic(expected = r#"tested_fixture::tests::panic_setup failed: "panicked""#)]
336    fn combine_panic() {
337        let _ = HeavySetup::build(SETUP_1.0 + SETUP_4.0);
338    }
339}