Insta is a great snapshot testing library for Rust.
In the past I've easily dismissed snapshot testing because it is always hard to maintain snapshots. Insta takes care of the vast majority of the work for you and provides sane diffing between snapshots.
Try It
cargo add --dev insta --features yaml --features glob
cargo install cargo-insta --locked
Add to a test file:
#[test]
fn test_foo() {
let text = "abc";
insta::assert_snapshot!(text);
}
Running:
cargo test
cargo insta review
That is it!
There are optimizations that can be done which are documented on the Insta website, but it really is that easy.
The biggest hiccup for me is that cargo insta
is close to cargo install
, but
it is relatively minor.
Insta vs Traditional Assertion Tests
Take traditional tests like:
#[test]
fn test_1() {
let output: Foo = do_something();
let expected = Foo {
value: 1,
};
assert_eq!(output, expected);
}
#[test]
fn test_2() {
let bar = Bar::new();
assert_eq!(bar.do_something(), "expected");
assert_eq!(bar.do_something_more(), 123);
assert_eq!(bar.do_something_else(), true);
}
The various expected states are stored in the test code (Foo { value: 1 }
,
"expected"
, 123
, true
). If code is changed so that the expected states are
now different, then the test code will have to be updated (e.g. change
"expected"
to "new_value"
). This is laborious and generally a bad use of any
person's time.
Instead, snapshot testing serializes the expected states which can be easily compared against prior runs.
#[test]
fn test_1() {
let output: Foo = do_something();
insta::assert_yaml_snapshot!(output);
}
#[test]
fn test_2() {
let output = Bar::new();
insta::assert_yaml_snapshot!(bar.do_something());
insta::assert_yaml_snapshot!(bar.do_something_more());
insta::assert_yaml_snapshot!(bar.do_something_else());
}
#[test]
fn test_2_alternative() {
let output = Bar::new();
let text = format!(
"{} {}",
bar.do_something(),
bar.do_something_more()
);
insta::assert_snapshot!(text);
}
Where Are the Expected Values
Looking at the above, it might be odd because there is no "expected" state.
Insta stores the state as plain text in .snap
files by default. The above
assertions use YAML (assert_yaml_snapshot
) and plain text (assert_snapshot
)
as the state serialization format. Insta allows several different snapshot
formats but most require the data to implement serde::Serialize
.
The .snap
files are easy to store in a version control system and easy to
compare against prior versions.
Of course, the conventions can be overridden. For example, if the snapshot files should be stored in some specific directory, that is possible.
If the snapshot data should be stored inline with the test code, that is also possible; Insta can even update the test code with the new serialized snapshot data via some magic.
What Happens If A Snapshot Differs
If a test is run and the generated snapshot differs to the existing .snap
snapshot, a new .snap.new
file is created in the same directory. To "accept"
the new snapshot, delete the old .snap
file and rename the .snap.new
file to
.snap
.
If you have many snapshots to accept/reject or want to see a good diff between
them, Insta provides an optional CLI that can be used to accept/reject the new
snapshots (cargo insta review
).
When a snapshot test fails, Insta provides a diff using its own custom library. The CLI tool shows the same diff making it quick to review new snapshots.
Development Speed
While it may not be completely obvious, development speed can be tremendously increased. If any code is changed and the expected state is different, the only update required for the test code is to review a diff. It's like accepting a change in a patch. Either accept or reject. There is no need to update many lines of test code for a simple change.
If there is new state that needs to verified, the only thing required is to add
a insta::assert_...snapshot!()
and then accept the new snapshot.
It is far faster to only:
- Review diffs
Than to have to:
- Review diffs
- Then also, update the code to the new accepted value.
Killer Feature - Globbing
The best advanced feature is Globbing
.
Usually, tests are repeated with just differences in the input and verifying the differences in the output. For instance, suppose there is a function like:
fn add(x: i32, y: i32) -> i32 {
x + y
}
Silly example, but various tests would pass in combinations of x
and y
and verifying the return value.
Instead of writing many tests, one test function can be used to read in input from a file and then a snapshot could be used to verify the output.
#[test]
fn test_add() {
insta::glob!("test_data/*.txt", |path| {
let text = fs::read_to_string(path).unwrap();
// test file could be "1 2" or "3 4"
let (x, y) = parse_input(&text);
insta::with_settings!({
description => &text,
}, {
insta::assert_snapshot!(add(x, y).to_string());
});
});
}
If new inputs need to be tested, it is as simple as adding a new text file with the new inputs.
It is easy to add new tests and even verify if a test is already covered or not by just comparing the input files.
Conclusion
Give Insta snapshot testing a try. I have not found many easier (snapshot testing) tools to work with.
(Note: I have no relation to the Insta project and am probably very far behind in recommending the library.)