Property-based snapshot testing integration for Protest and Insta.
protest-insta combines the power of property-based testing with snapshot testing, allowing you to:
- Test with diverse inputs while maintaining visual regression testing
- Detect unexpected changes in serialization, formatting, or computation results
- Document behavior through automatically captured snapshots
- Review changes using Insta's powerful review workflow
Add to your Cargo.toml:
[dev-dependencies]
protest = "0.1"
protest-insta = "0.1"
insta = "1.41"
serde = { version = "1.0", features = ["derive"] }use protest::{Generator, primitives::IntGenerator, config::GeneratorConfig};
use protest_insta::PropertySnapshots;
use serde::Serialize;
use rand::SeedableRng;
use rand::rngs::StdRng;
#[derive(Serialize)]
struct Point { x: i32, y: i32 }
#[test]
fn test_point_serialization() {
let mut rng = StdRng::seed_from_u64(42);
let config = GeneratorConfig::default();
let generator = IntGenerator::new(0, 100);
let mut snapshots = PropertySnapshots::new("point_serialization");
for _ in 0..5 {
let x = generator.generate(&mut rng, &config);
let y = generator.generate(&mut rng, &config);
let point = Point { x, y };
snapshots.assert_json_snapshot(&point);
}
}The PropertySnapshots struct manages multiple snapshots with automatic sequential naming:
use protest_insta::PropertySnapshots;
let mut snapshots = PropertySnapshots::new("my_test");
// Creates snapshots named: my_test_0, my_test_1, my_test_2, ...
snapshots.assert_json_snapshot(&data1);
snapshots.assert_json_snapshot(&data2);
snapshots.assert_debug_snapshot(&data3);Perfect for testing serialization of structured data:
use serde::Serialize;
#[derive(Serialize)]
struct Config {
port: u16,
host: String,
debug: bool,
}
let config = Config {
port: 8080,
host: "localhost".to_string(),
debug: true
};
snapshots.assert_json_snapshot(&config);Great for testing computation results and non-serializable types:
let results: Vec<i32> = vec![1, 2, 3, 4, 5];
snapshots.assert_debug_snapshot(&results);For YAML-formatted snapshots:
use serde::Serialize;
#[derive(Serialize)]
struct Settings {
timeout: u64,
retries: u8,
}
let settings = Settings { timeout: 30, retries: 3 };
snapshots.assert_yaml_snapshot(&settings);The property_snapshot_test function provides a concise API:
use protest::primitives::IntGenerator;
use protest_insta::property_snapshot_test;
use serde::Serialize;
#[derive(Serialize)]
struct Square { value: i32, squared: i32 }
#[test]
fn test_squaring() {
property_snapshot_test(
"square_function",
IntGenerator::new(1, 10),
5, // sample count
42, // seed
|value, snapshots| {
let squared = value * value;
let result = Square { value, squared };
snapshots.assert_json_snapshot(&result);
}
);
}Test that your types serialize consistently across different inputs:
use protest::primitives::VecGenerator;
use serde::Serialize;
#[derive(Serialize)]
struct Report {
data: Vec<i32>,
summary: String,
}
#[test]
fn test_report_serialization() {
let generator = VecGenerator::new(IntGenerator::new(0, 100), 1, 10);
let mut snapshots = PropertySnapshots::new("reports");
// Test with various vector sizes and contents
for _ in 0..5 {
let data = generator.generate(&mut rng, &config);
let report = Report {
data: data.clone(),
summary: format!("Count: {}", data.len()),
};
snapshots.assert_json_snapshot(&report);
}
}Verify API responses remain stable:
#[derive(Serialize)]
struct ApiResponse {
status: u16,
body: String,
headers: HashMap<String, String>,
}
#[test]
fn test_api_responses() {
let mut snapshots = PropertySnapshots::new("api_responses");
for status in [200, 404, 500] {
let response = create_response(status);
snapshots.assert_json_snapshot(&response);
}
}Document computation behavior across inputs:
#[test]
fn test_factorial_outputs() {
property_snapshot_test(
"factorial",
IntGenerator::new(1, 10),
10,
123,
|n, snapshots| {
let result = factorial(n);
snapshots.assert_debug_snapshot(&result);
}
);
}Detect unexpected changes in output format:
#[test]
fn test_markdown_generation() {
let mut snapshots = PropertySnapshots::new("markdown");
for _ in 0..5 {
let document = generate_document(&mut rng);
let markdown = document.to_markdown();
snapshots.assert_debug_snapshot(&markdown);
}
}Always use a seeded RNG for reproducible snapshots:
use rand::SeedableRng;
use rand::rngs::StdRng;
let mut rng = StdRng::seed_from_u64(42); // ✅ Reproducible
// let mut rng = rand::thread_rng(); // ❌ Non-deterministicKeep snapshot counts reasonable (5-10) to make reviews manageable:
let mut snapshots = PropertySnapshots::new("test");
for _ in 0..5 { // ✅ Reasonable
// ...
}
// for _ in 0..1000 { // ❌ Too many snapshotsChoose clear, descriptive names for snapshot groups:
PropertySnapshots::new("user_profile_json") // ✅ Clear
PropertySnapshots::new("test1") // ❌ UnclearUse the same base name for related test scenarios:
let mut snapshots = PropertySnapshots::new("sorting_algorithms");
snapshots.assert_debug_snapshot(&bubble_sort_result);
snapshots.assert_debug_snapshot(&quick_sort_result);
snapshots.assert_debug_snapshot(&merge_sort_result);Use Insta's review workflow:
# Review all pending snapshots
cargo insta review
# Accept all snapshots
cargo insta accept
# Reject all snapshots
cargo insta reject#[test]
fn test_serialization() {
let data = MyStruct { value: 42 };
insta::assert_json_snapshot!(data);
}Limitations:
- Tests only one fixed input
- May miss edge cases
- Requires manual input selection
#[test]
fn test_serialization_property_based() {
property_snapshot_test(
"serialization",
IntGenerator::new(0, 1000),
10,
42,
|value, snapshots| {
let data = MyStruct { value };
snapshots.assert_json_snapshot(&data);
}
);
}Benefits:
- Tests multiple diverse inputs automatically
- Discovers edge cases
- Better coverage with less code
See the examples/ directory for complete working examples:
json_snapshots.rs- JSON snapshot testing with complex structuresdebug_snapshots.rs- Debug snapshots for computation resultsproperty_snapshot_test.rs- Using the helper function
Run examples with:
cargo run --example json_snapshots
cargo run --example debug_snapshots
cargo run --example property_snapshot_testManages a group of related snapshots with automatic naming.
new(base_name)- Create a new snapshot helperassert_json_snapshot(&value)- Create a JSON snapshotassert_debug_snapshot(&value)- Create a debug snapshotassert_yaml_snapshot(&value)- Create a YAML snapshotreset()- Reset the counter to 0count()- Get the current counter value
Helper function for concise property-based snapshot testing.
test_name: &str- Base name for snapshotsgenerator: G- Generator for test inputssample_count: usize- Number of samples to generateseed: u64- RNG seed for reproducibilitytest_fn: F- Test function receiving each generated value
This crate is built on top of Insta, so all Insta features work seamlessly:
- Snapshot review workflow -
cargo insta review - Inline snapshots - Use Insta's inline snapshot macros
- Settings - Configure Insta with
insta::Settings - Filters - Apply Insta's redaction filters
A: Use Insta's CLI tool:
cargo install cargo-insta
cargo insta reviewA: By default, in a snapshots/ directory next to your test file. Insta manages this automatically.
A: Yes! Snapshots are part of your test suite and should be version controlled.
A: Start with 5-10. More samples give better coverage but make reviews more tedious.
A: Absolutely! protest-insta is just a helper layer on top of Insta. Mix and match freely.
Contributions are welcome! Please see the main Protest repository for contribution guidelines.
Licensed under the MIT license. See LICENSE for details.