Property-based Testing in Rust: How to Catch Bugs You Weren't Looking For

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Property-based Testing in Rust: The Art of Catching Bugs You Weren't Looking For

\n\n

Have you ever written unit tests, run them, everything green, and then the application crashed on the very first non-standard user input? Sounds familiar. Classic tests are like checking if a lock works by only inserting your own key. But what if an attacker (or just chance) brings a screwdriver, a paperclip, or a piece of wire?

\n\n

In Rust, a language renowned for its safety, there is a powerful tool that allows you not just to check specific scenarios, but to prove that code behaves correctly for all possible inputs. This is property-based testing. And today we'll break it down in practice, with real examples, so you can immediately apply this knowledge in your projects.

\n\n

What is Property-based Testing and How is it Different from Regular Testing?

\n\n

Imagine you are testing a sorting function. A typical unit test looks like this:

\n\n
fn test_sort() {\n    let mut arr = vec![3, 1, 2];\n    arr.sort();\n    assert_eq!(arr, vec![1, 2, 3]);\n}
\n\n

Does it work? It works. But what if the array is already sorted? What if it's empty? What if it has 10,000 elements in reverse order? Writing tests for every case manually is a path to infinity.

\n\n

Property-based testing flips the approach. Instead of specifying concrete input data, you describe a property that must hold for any valid input. A special library (e.g., proptest or quickcheck) then generates hundreds and thousands of random test cases, trying to find that one case that violates your property.

\n\n

For sorting, a property could be: "The sorted result always has the same length as the original array, and each element from the original array appears in the result exactly once." Or more simply: "The result is sorted in ascending order."

\n\n

Why Rust is an Ideal Environment for Property-based Testing

\n\n

Rust already catches many errors at compile time (memory ownership, data races). But there is a class of bugs the compiler doesn't see: logical errors, incorrect handling of edge cases, unexpected behavior with specific data combinations. This is where property-based testing in Rust comes into play.

\n\n

It perfectly complements the type system: types guarantee that data has the correct structure, while property-based tests ensure that the logic for processing that data is correct across the entire space of possible values.

\n\n

Tooling: proptest vs quickcheck

\n\n

In the Rust ecosystem, there are two main frameworks: proptest and quickcheck. Both do the same thing, but with different philosophies.

\n\n
    \n
  • quickcheck is older, more minimalistic. It works via the Arbitrary trait. You write a function that takes random data, and the test runs many times.
  • \n
  • proptest is modern, powerful, with a plethora of data generation strategies. It allows fine-tuning what data to generate and can "shrink" a failing example down to the minimal reproducible case. It is the current gold standard.
  • \n
\n\n

We will use proptest, as it gives more control and is better documented. Let's install it:

\n\n
# Cargo.toml\n[dev-dependencies]\nproptest = "1.4"
\n\n

Writing Your First Property-based Test in Rust

\n\n

Let's go back to our sorting function. Let's write a test that checks that the result is indeed sorted:

\n\n
use proptest::prelude::*;\n\nfn is_sorted<T: Ord>(slice: &[T]) -> bool {\n    slice.windows(2).all(|w| w[0] <= w[1])\n}\n\nproptest!                

Blogs

Book Recommendations