protest_insta/
lib.rs

1//! Property-Based Snapshot Testing with Insta
2//!
3//! This crate provides integration between [Protest](https://crates.io/crates/protest)
4//! property-based testing and [Insta](https://crates.io/crates/insta) snapshot testing.
5//!
6//! # Overview
7//!
8//! Property-based snapshot testing allows you to:
9//! - Test with **diverse, generated inputs** while keeping visual regression testing
10//! - **Detect unexpected changes** in serialization output
11//! - **Document behavior** through automatically captured snapshots
12//! - **Review changes** using Insta's review workflow
13//!
14//! # Quick Start
15//!
16//! ```rust,no_run
17//! use protest::{Generator, primitives::IntGenerator, config::GeneratorConfig};
18//! use protest_insta::PropertySnapshots;
19//! use serde::Serialize;
20//! use rand::SeedableRng;
21//! use rand::rngs::StdRng;
22//!
23//! #[derive(Serialize)]
24//! struct Point { x: i32, y: i32 }
25//!
26//! #[test]
27//! fn test_point_serialization() {
28//!     let mut rng = StdRng::seed_from_u64(42);
29//!     let config = GeneratorConfig::default();
30//!     let generator = IntGenerator::new(0, 100);
31//!
32//!     let mut snapshots = PropertySnapshots::new("point_serialization");
33//!
34//!     for _ in 0..5 {
35//!         let x = generator.generate(&mut rng, &config);
36//!         let y = generator.generate(&mut rng, &config);
37//!         let point = Point { x, y };
38//!         snapshots.assert_json_snapshot(&point);
39//!     }
40//! }
41//! ```
42//!
43//! # Features
44//!
45//! - **Generator Integration**: Use any Protest generator for snapshot inputs
46//! - **Automatic Naming**: Sequential snapshot names for property-based tests
47//! - **JSON Support**: Built-in JSON serialization for snapshots
48//! - **Debug Support**: Snapshot any Debug type
49//! - **Insta Workflow**: Full compatibility with Insta's review tools
50//!
51//! # Examples
52//!
53//! ## JSON Snapshot Testing
54//!
55//! ```rust,no_run
56//! use protest::{Generator, primitives::VecGenerator, primitives::IntGenerator};
57//! use protest::{config::GeneratorConfig};
58//! use protest_insta::PropertySnapshots;
59//! use serde::Serialize;
60//! use rand::SeedableRng;
61//! use rand::rngs::StdRng;
62//!
63//! #[derive(Serialize)]
64//! struct Data {
65//!     values: Vec<i32>,
66//!     sum: i32,
67//! }
68//!
69//! #[test]
70//! fn test_data_processing() {
71//!     let mut rng = StdRng::seed_from_u64(123);
72//!     let config = GeneratorConfig::default();
73//!     let generator = VecGenerator::new(IntGenerator::new(0, 10), 1, 5);
74//!
75//!     let mut snapshots = PropertySnapshots::new("data_processing");
76//!
77//!     for _ in 0..3 {
78//!         let values = generator.generate(&mut rng, &config);
79//!         let sum: i32 = values.iter().sum();
80//!         let data = Data { values, sum };
81//!         snapshots.assert_json_snapshot(&data);
82//!     }
83//! }
84//! ```
85//!
86//! ## Debug Snapshot Testing
87//!
88//! ```rust,no_run
89//! use protest::{Generator, primitives::IntGenerator, config::GeneratorConfig};
90//! use protest_insta::PropertySnapshots;
91//! use rand::SeedableRng;
92//! use rand::rngs::StdRng;
93//!
94//! #[test]
95//! fn test_computation_output() {
96//!     let mut rng = StdRng::seed_from_u64(456);
97//!     let config = GeneratorConfig::default();
98//!     let generator = IntGenerator::new(1, 100);
99//!
100//!     let mut snapshots = PropertySnapshots::new("computation");
101//!
102//!     for _ in 0..5 {
103//!         let n = generator.generate(&mut rng, &config);
104//!         let result = (1..=n).sum::<i32>();
105//!         snapshots.assert_debug_snapshot(&result);
106//!     }
107//! }
108//! ```
109
110use serde::Serialize;
111use std::fmt::Debug;
112
113/// Helper for managing property-based snapshots
114///
115/// This struct provides convenient methods for creating multiple snapshots
116/// in property-based tests. Each snapshot gets an automatic sequential name.
117///
118/// # Example
119///
120/// ```rust,no_run
121/// use protest::{Generator, primitives::IntGenerator, config::GeneratorConfig};
122/// use protest_insta::PropertySnapshots;
123/// use rand::SeedableRng;
124/// use rand::rngs::StdRng;
125///
126/// #[test]
127/// fn test_with_property_snapshots() {
128///     let mut rng = StdRng::seed_from_u64(42);
129///     let config = GeneratorConfig::default();
130///     let generator = IntGenerator::new(0, 100);
131///
132///     let mut snapshots = PropertySnapshots::new("test_case");
133///
134///     for _ in 0..5 {
135///         let value = generator.generate(&mut rng, &config);
136///         snapshots.assert_debug_snapshot(&value);
137///     }
138/// }
139/// ```
140pub struct PropertySnapshots {
141    /// Base name for all snapshots in this group
142    base_name: String,
143    /// Counter for sequential snapshot naming
144    counter: usize,
145}
146
147impl PropertySnapshots {
148    /// Create a new PropertySnapshots helper with the given base name
149    ///
150    /// # Arguments
151    ///
152    /// * `base_name` - The base name for all snapshots in this group
153    ///
154    /// # Example
155    ///
156    /// ```rust
157    /// use protest_insta::PropertySnapshots;
158    ///
159    /// let mut snapshots = PropertySnapshots::new("my_test");
160    /// // Will create snapshots named: my_test_0, my_test_1, my_test_2, ...
161    /// ```
162    pub fn new(base_name: impl Into<String>) -> Self {
163        Self {
164            base_name: base_name.into(),
165            counter: 0,
166        }
167    }
168
169    /// Get the next snapshot name and increment the counter
170    fn next_name(&mut self) -> String {
171        let name = format!("{}_{}", self.base_name, self.counter);
172        self.counter += 1;
173        name
174    }
175
176    /// Assert a JSON snapshot for the given value
177    ///
178    /// This serializes the value to pretty-printed JSON and creates a snapshot.
179    ///
180    /// # Arguments
181    ///
182    /// * `value` - The value to snapshot (must implement Serialize)
183    ///
184    /// # Example
185    ///
186    /// ```rust,no_run
187    /// use protest_insta::PropertySnapshots;
188    /// use serde::Serialize;
189    ///
190    /// #[derive(Serialize)]
191    /// struct Config { port: u16, host: String }
192    ///
193    /// # fn test() {
194    /// let mut snapshots = PropertySnapshots::new("config");
195    /// let cfg = Config { port: 8080, host: "localhost".to_string() };
196    /// snapshots.assert_json_snapshot(&cfg);
197    /// # }
198    /// ```
199    pub fn assert_json_snapshot<T: Serialize>(&mut self, value: &T) {
200        let name = self.next_name();
201        insta::assert_json_snapshot!(name, value);
202    }
203
204    /// Assert a debug snapshot for the given value
205    ///
206    /// This uses the Debug trait to format the value and creates a snapshot.
207    ///
208    /// # Arguments
209    ///
210    /// * `value` - The value to snapshot (must implement Debug)
211    ///
212    /// # Example
213    ///
214    /// ```rust,no_run
215    /// use protest_insta::PropertySnapshots;
216    ///
217    /// # fn test() {
218    /// let mut snapshots = PropertySnapshots::new("numbers");
219    /// let values = vec![1, 2, 3, 4, 5];
220    /// snapshots.assert_debug_snapshot(&values);
221    /// # }
222    /// ```
223    pub fn assert_debug_snapshot<T: Debug>(&mut self, value: &T) {
224        let name = self.next_name();
225        insta::assert_debug_snapshot!(name, value);
226    }
227
228    /// Assert a YAML snapshot for the given value
229    ///
230    /// This serializes the value to YAML and creates a snapshot.
231    ///
232    /// # Arguments
233    ///
234    /// * `value` - The value to snapshot (must implement Serialize)
235    ///
236    /// # Example
237    ///
238    /// ```rust,no_run
239    /// use protest_insta::PropertySnapshots;
240    /// use serde::Serialize;
241    ///
242    /// #[derive(Serialize)]
243    /// struct Settings { debug: bool, timeout: u64 }
244    ///
245    /// # fn test() {
246    /// let mut snapshots = PropertySnapshots::new("settings");
247    /// let s = Settings { debug: true, timeout: 30 };
248    /// snapshots.assert_yaml_snapshot(&s);
249    /// # }
250    /// ```
251    pub fn assert_yaml_snapshot<T: Serialize>(&mut self, value: &T) {
252        let name = self.next_name();
253        insta::assert_yaml_snapshot!(name, value);
254    }
255
256    /// Reset the counter back to 0
257    ///
258    /// Useful if you want to reuse the same PropertySnapshots instance
259    /// with a different set of tests.
260    ///
261    /// # Example
262    ///
263    /// ```rust
264    /// use protest_insta::PropertySnapshots;
265    ///
266    /// let mut snapshots = PropertySnapshots::new("test");
267    /// // ... use snapshots ...
268    /// snapshots.reset();
269    /// // Counter is back to 0
270    /// ```
271    pub fn reset(&mut self) {
272        self.counter = 0;
273    }
274
275    /// Get the current counter value
276    ///
277    /// # Example
278    ///
279    /// ```rust
280    /// use protest_insta::PropertySnapshots;
281    ///
282    /// let mut snapshots = PropertySnapshots::new("test");
283    /// assert_eq!(snapshots.count(), 0);
284    /// ```
285    pub fn count(&self) -> usize {
286        self.counter
287    }
288}
289
290/// Helper function to run a property-based test with snapshot assertions
291///
292/// This function generates multiple inputs using the provided generator
293/// and calls the test function for each input. The test function should
294/// use Insta's snapshot assertions.
295///
296/// # Arguments
297///
298/// * `test_name` - Base name for the snapshots
299/// * `generator` - Generator for creating test inputs
300/// * `sample_count` - Number of samples to generate
301/// * `seed` - RNG seed for reproducibility
302/// * `test_fn` - Function to test each generated value
303///
304/// # Example
305///
306/// ```rust,no_run
307/// use protest::{Generator, primitives::IntGenerator, config::GeneratorConfig};
308/// use protest_insta::property_snapshot_test;
309/// use serde::Serialize;
310///
311/// #[derive(Serialize)]
312/// struct Square { value: i32, squared: i32 }
313///
314/// #[test]
315/// fn test_squaring() {
316///     property_snapshot_test(
317///         "square_function",
318///         IntGenerator::new(1, 10),
319///         5,
320///         42,
321///         |value, snapshots| {
322///             let squared = value * value;
323///             let result = Square { value, squared };
324///             snapshots.assert_json_snapshot(&result);
325///         }
326///     );
327/// }
328/// ```
329pub fn property_snapshot_test<T, G, F>(
330    test_name: &str,
331    generator: G,
332    sample_count: usize,
333    seed: u64,
334    mut test_fn: F,
335) where
336    G: protest::Generator<T>,
337    F: FnMut(T, &mut PropertySnapshots),
338{
339    use protest::config::GeneratorConfig;
340    use rand::SeedableRng;
341    use rand::rngs::StdRng;
342
343    let mut rng = StdRng::seed_from_u64(seed);
344    let config = GeneratorConfig::default();
345    let mut snapshots = PropertySnapshots::new(test_name);
346
347    for _ in 0..sample_count {
348        let value = generator.generate(&mut rng, &config);
349        test_fn(value, &mut snapshots);
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356
357    #[test]
358    fn test_snapshot_naming() {
359        let mut snapshots = PropertySnapshots::new("test");
360        assert_eq!(snapshots.next_name(), "test_0");
361        assert_eq!(snapshots.next_name(), "test_1");
362        assert_eq!(snapshots.next_name(), "test_2");
363    }
364
365    #[test]
366    fn test_snapshot_reset() {
367        let mut snapshots = PropertySnapshots::new("test");
368        snapshots.next_name();
369        snapshots.next_name();
370        assert_eq!(snapshots.count(), 2);
371        snapshots.reset();
372        assert_eq!(snapshots.count(), 0);
373        assert_eq!(snapshots.next_name(), "test_0");
374    }
375
376    #[test]
377    fn test_snapshot_count() {
378        let mut snapshots = PropertySnapshots::new("test");
379        assert_eq!(snapshots.count(), 0);
380        snapshots.next_name();
381        assert_eq!(snapshots.count(), 1);
382        snapshots.next_name();
383        assert_eq!(snapshots.count(), 2);
384    }
385}