Insta - Snapshot Testing for Rust

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:

  1. Review diffs

Than to have to:

  1. Review diffs
  2. 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.)