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}