Rust testing libraries You should know about

Cargo provides a great built-in testing framework, it's powerful and sometimes sufficient, but it can be extended with other testing libraries to provide additional features like property-based testing, Snapshot Testing, mocking, and more.

In this blog post, we're going to explore some of the most popular testing libraries in Rust, it's important that you know that these libraries exist even if you don't use them right away, as they can be very useful in certain situations and when you detect those situations you'll know exactly what to do.

So, let's get started!

Property-Based Testing

Property-based testing is a testing approach that generates random inputs to test the properties of a program instead of writing specific test cases. It makes it easier to write exhaustive tests and finding edge cases that might be missed by traditional unit testing.

Here are some of the most popular property-based testing libraries in Rust:

Learn Rust by Practice

Master Rust through hands-on coding exercises and real-world examples.

Proptest

The proptest crate is a property-based testing framework for Rust, inspired by the Hypothesis framework for Python.

Property-based testing is basically a testing approach that generates random inputs to test the properties of a program instead of writing specific test cases.

proptest allows developers to test their code by generating random inputs and checking that certain properties hold for all inputs. If a failure is found, proptest automatically shrinks the input to find the minimal failing test case.

Let's demonstrate that in an example.

Proptest Example

Let's say we have a function parse_date() and we want to write extensive tests for it.

Without proptest, we would have to write individual test cases for different dates, like this:

#[test]
fn test_parse_date() {
    assert_eq!(parse_date("2021-01-01"), Some((2021, 1, 1)));
    assert_eq!(parse_date("2021-02-28"), Some((2021, 2, 28)));
    assert_eq!(parse_date("2021-02-29"), None);
    assert_eq!(parse_date("2021-12-31"), Some((2021, 12, 31)));
}

While this can be useful, it's not exhaustive and doesn't cover all possible edge cases, and definitely we're not going to write all the possible combinations in the universe in our test cases.

Instead of writing individual test cases, we can use proptest to generate random dates by itself, without us having to do it manually.

Here's an example of using proptest to test a simple date parsing function:

use proptest::prelude::*;
 
proptest! {
    #[test]
    fn parses_date_back_to_original(
      y in 0u32..10000,
      m in 1u32..13,
      d in 1u32..32
    ) {
      let (y2, m2, d2) = parse_date(&format!("{:04}-{:02}-{:02}", y, m, d)).unwrap();
      prop_assert_eq!((y, m, d), (y2, m2, d2));
    }
}

Syntax

Here's how the syntax works:

  • proptest! is a macro that defines a property-based test.
  • 0u32..10000 generates a random u32 value between 0 and 9999.
  • 1u32..13 generates a random u32 value between 1 and 12.
  • 1u32..32 generates a random u32 value between 1 and 31.

This code generates all possible combinations of y, m, and d in the specified ranges and tests that the parsed date is equal to the original date.

proptest is designed to complement traditional unit testing by automatically generating a wide range of test inputs. It is particularly useful for uncovering edge cases that may not be immediately obvious.

Proptest Limitations

  • Performance: Generating complex values in proptest can be slower compared to other property testing frameworks like QuickCheck, due to its richer shrinking model.
  • Edge Case Coverage: Property testing is unlikely to find specific edge cases in large input spaces without extensive testing time.

proptest is a great tool for you to use to enhance your testing strategy with property-based testing. It offers powerful capabilities to test code against a wide range of inputs making exhaustive tests easier to write.


QuickCheck

QuickCheck just like proptest is a property-based testing framework. It allows for testing functions by generating random inputs and checking that a specified property holds for all inputs.

If the property fails, quickcheck automatically shrinks the input to find the minimal counterexample that causes the failure.

QuickCheck Example

Here's a simple example testing a function that reverses a vector:

#[cfg(test)]
#[macro_use]
extern crate quickcheck;
 
fn reverse<T: Clone>(xs: &[T]) -> Vec<T> {
    let mut rev = vec![];
    for x in xs.iter() {
        rev.insert(0, x.clone())
    }
    rev
}
 
#[cfg(test)]
mod tests {
    quickcheck! {
        fn prop(xs: Vec<u32>) -> bool {
            xs == reverse(&reverse(&xs))
        }
    }
}

This example uses the quickcheck! macro to verify that reversing a vector twice results in the original vector.

Comparison with proptest

While quickcheck is efficient and straightforward, proptest offers more advanced shrinking capabilities and finer control over input generation. If shrinking behavior in quickcheck is a concern, consider using proptest as an alternative.

quickcheck is an excellent tool for introducing property-based testing into Rust projects, making it easier to find and debug edge cases that might be missed by traditional unit testing.

Snapshot Testing

Snapshot Testing is a testing technique that captures the output of a function and compares it to a previously stored snapshot. If the output changes, the test fails, and the developer must decide whether to update the snapshot or investigate the change.

Snapshot testing is particularly useful when testing complex data structures or large outputs that are difficult to verify manually.

Here are some popular snapshot testing libraries in Rust:

Insta

The insta crate is a snapshot testing library for Rust. It allows developers to perform snapshot tests, which are useful for comparing complex values against reference snapshots, especially when those values are large or frequently changing.

Insta Example

Le'ts say we have a function that needs to return a large array of numbers in a specific order, and we want to test it. Here's what we have to do:

  • Define your function.
  • Write the snapshot testing for the function.
  • Run the test for the first time (it will generate a new snapshot).
  • Review the snapshot and accept it.
  • Run the test again, this time it will compare the new output with the snapshot that has been saved, if they match, the test is marked as passed, and if they differ, the test is marked as failed.

Here's a basic example of how to use insta for snapshot testing, let's define the function first, we'll define a simple function that returns a vector of numbers from 1 to 100:

fn generate_large_array() -> Vec<u32> {
    (1..=100).collect() // A simple function that returns numbers from 1 to 100
}

Now, if we write the test, it will not be convenient to write whe whole expected vector in the test case, so we can use insta to generate a snapshot for us.

This is doable but extremely inconvenient and in some situations impossible:

#[test]
fn test_generate_large_array() {
    assert_eq!(generate_large_array(),
    vec![1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100]); // This is not convenient and impractical
}

Instead, we can use insta to generate the snapshots for us, here's how to do it:

#[cfg(test)]
mod tests {
    use super::*;
 
    #[test]
    fn test_generate_large_array() {
        let array = generate_large_array();
        insta::assert_debug_snapshot!(array);
    }
}

Run the test, this will generate the snapshot for the first time but will fail the test until you review the snapshot and approve it:

cargo test

Insta Snapshot Insta Snapshot

Insta tells us to run the command cargo insta review to review all new snapshots, to do that, you must first install the cargo-insta tool:

cargo install cargo-insta --version 1.15.0 --locked

Or, it's best if you visit their GitHub instructions page.

After installation, run the review command:

cargo insta review

This will give you the option to accept, or reject the snapshot, if you accept it, the subsequent test runs will compare against the snapshot you've just accepted.

Insta Snapshot Review Insta Snapshot Review

Insta Features

This is just tip of the iceberg, insta has many features that make it a powerful snapshot testing library:

  • Inline Snapshots: Snapshots can be stored directly in the source file with the help of the cargo-insta tool.
  • Editor Support: There's a VSCode extension for viewing and managing .snap files.
  • Diffing: Uses the similar crate for diffing operations, which can also be used independently with similar-asserts.

insta is widely adopted and provides powerful tools for maintaining accurate and up-to-date tests, making it a valuable asset for Rust developers who need precise and comprehensive snapshot testing capabilities.

CLI Testing

Command-line interfaces (CLIs) also need to be tested, testing them manually can be cumbersome and error-prone. Here are some libraries that can help you test your CLI applications:

assert_cmd

assert_cmd is a Rust crate designed to simplify integration testing of CLI applications, it achieves this by:

  • Finding the binary of the crate to test.
  • Asserting the success or failure of the program's execution.

You can easily test your CLI applications, here's an example:

use assert_cmd::Command;
 
let mut cmd = Commad::cargo_bin("my-cli-app").unwrap();
cmd.assert().success();

In this test, we are expecting the command my-cli-app to run successfully and return an exit code of 0.

There is plenty of other things you can do with it, read the documentation to learn more about it.

Conclusion

In this blog post, we've explored some of the most popular testing libraries in Rust, including property-based testing with proptest and quickcheck, snapshot testing with insta, and CLI testing with assert_cmd.

Testing is an essential part of developing robust software, and these libraries can contribute to your testing strategy by providing additional features and testing strategies that complement traditional unit testing.

Thank you for reading! If you enjoyed this blog post, you might also like to try out our Rust practice section where you can solve Rust exercises and learn by doing.

Good luck with your Rust testing journey! 🦀🧪

Learn Rust by Practice

Master Rust through hands-on coding exercises and real-world examples.

Check out our blog

Discover more insightful articles and stay up-to-date with the latest trends.

Subscribe to our newsletter

Get the latest updates and exclusive content delivered straight to your inbox.