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}