checkito 3.2.3
A safe, efficient and simple QuickCheck-inspired library to generate shrinkable random data mainly oriented towards generative/property/exploratory testing.
---
### In Brief
The purpose of the library is to test general properties of a program rather than very specific examples as you would with unit tests.
- When writing a `checkito` test (called a `check`), you first construct a generator by specifying the bounds that make sense for the inputs (ex: a number in the range `10..100`, an alpha-numeric string, a vector of `f64`, etc.).
- Generators can produce arbitrary complex values with its combinators in a similar way that `Iterator`s can.
- Given a proper generator, `checkito` will sample the input space to find a failing case for your test.
- Once a failing case is found, `checkito` will try to reduce the input to the simplest version of it that continues to fail (using a kind of binary search of the input space) to make the debugging process much easier.
- Note that `checkito` does not guarantee any kind of exhaustive search of the input space (the size of it gets out of hand rather quickly) and is meant as a complement to other testing strategies.
- It is recommended to write a regular unit test with the exact failing input to prevent a regression and to truly guarantee that the failing input is always tested.
---
### Main Traits
- [`Generate`](src/generate.rs): is implemented for many of rust's standard types and allows the generation of any random composite/structured data through combinator (such as tuples, [`Any`](src/any.rs), [`Map`](src/map.rs), [`Flatten`](src/flatten.rs) and more). It is designed for composability and its usage should feel like working with `Iterator`s.
- [`Shrink`](src/shrink.rs): tries to reduce a generated sample to a 'smaller' version of it while maintaining its constraints (ex: a sample `usize` in the range `10..100` will never be shrunk below `10`). For numbers, it means bringing the sample closer to 0, for vectors, it means removing irrelevant items and shrinking the remaining ones, and so on.
- [`Prove`](src/prove.rs): represents a desirable property of a program under test. It is used mainly in the context of the [`Check::check`](src/check.rs) or [`Checker::check`](src/check.rs) methods and it is the failure of a proof that triggers the shrinking process. It is implemented for a couple of standard types such as `()`, `bool` and `Result`. A `panic!()` is also considered as a failing property, thus standard `assert!()` macros (or any other panicking assertions) can be used to check the property.
*To ensure safety, this library is `#![forbid(unsafe_code)]`.*
---
### Cheat Sheet
```rust
use checkito::*;
/// The `#[check]` attribute is designed to be as thin as possible and
/// everything that is expressible with it is also ergonomically expressible as
/// _regular_ code (see below). Each `#[check]` attribute expands to a single
/// function call.
///
/// An empty `#[check]` attribute acts just like `#[test]`. It is allowed for
/// consistency between tests.
#[check]
fn empty() {}
/// The builtin `letter()` generator will yield ascii letters.
///
/// This test will be run many times with different generated values to find a
/// failing input.
#[check(letter())]
fn is_letter(value: char) {
assert!(value.is_ascii_alphabetic());
}
/// Ranges can be used as generators and will yield values within its bounds.
///
/// A [`bool`] can be returned and if `true`, it will be considered as evidence
/// that the property under test holds.
#[check(0usize..=100)]
fn is_in_range(value: usize) -> bool {
value <= 100
}
/// Regexes can be used and validated either dynamically using the [`regex`]
/// generator or at compile-time with the [`regex!`] macro.
///
/// Usual panicking assertions can be used in the body of the checking function
/// since a panic is considered a failed property.
#[check(regex("{", None).ok(), regex!("[a-zA-Z0-9_]*"))]
fn is_ascii(invalid: Option, valid: String) {
assert!(invalid.is_none());
assert!(valid.is_ascii());
}
/// The `_` and `..` operators can be used to infer the [`FullGenerate`]
/// generator implementation for a type. Specifically, the `..` operator works
/// the same way as slice match patterns.
///
/// Since this test will panic, `#[should_panic]` can be used in the usual way.
#[check(..)]
#[check(_, _, _, _)]
#[check(negative::(), ..)]
#[check(.., negative::())]
#[check(_, .., _)]
#[check(negative::(), _, .., _, negative::())]
#[should_panic]
fn is_negative(first: f64, second: i8, third: isize, fourth: i16) {
assert!(first < 0.0);
assert!(second < 0);
assert!(third < 0);
assert!(fourth < 0);
}
/// `color = false` disables coloring of the output.
/// `verbose = true` will display all the steps taken by the [`check::Checker`]
/// while generating and shrinking values.
///
/// The shrinking process is pretty good at finding minimal inputs to reproduce
/// a failing property and in this case, it will always shrink values over
/// `1000` to exactly `1000`.
#[check(0u64..1_000_000, color = false, verbose = true)]
#[should_panic]
fn is_small(value: u64) {
assert!(value < 1000);
}
/// Multiple checks can be performed.
///
/// If all generators always yield the same value, the check becomes a
/// parameterized unit test and will run only once.
#[check(3001, 6000)]
#[check(4500, 4501)]
#[check(9000, 1)]
fn sums_to_9001(left: i32, right: i32) {
assert_eq!(left + right, 9001);
}
/// Generics can be used as inputs to the checking function.
///
/// [`Generate::map`] can be used to map a value to another.
#[check(111119)]
#[check(Generate::map(10..1000, |value| value * 10 - 1))]
#[check("a string that ends with 9")]
#[check(regex!("[a-z]*9"))]
fn ends_with_9(value: impl std::fmt::Display) -> bool {
format!("{value}").ends_with('9')
}
pub struct Person {
pub name: String,
pub age: usize,
}
/// Use tuples to combine generators and build more complex structured types.
/// Alternatively implement the [`FullGenerate`] trait for the [`Person`]
/// struct.
///
/// Any generator combinator can be used here; see the other examples in the
/// _examples_ folder for more details.
///
/// Disable `debug` if a generated type does not implement [`Debug`] which
/// removes the only requirement that `#[check]` requires from input types.
#[check((letter().collect(), 18usize..=100).map(|(name, age)| Person { name, age }), debug = false)]
fn person_has_valid_name_and_is_major(person: Person) {
assert!(person.name.is_ascii());
assert!(person.age >= 18);
}
/// The `#[check]` attribute essentially expands to a call to [`Check::check`]
/// with pretty printing. For some more complex scenarios, it may become more
/// convenient to simply call the [`Check::check`] manually.
///
/// The [`Generate::any`] combinator chooses from its inputs. The produced
/// `Or<..>` preserves the information about the choice but here, it can be
/// simply collapsed using [`Generate::unify`].
#[test]
fn has_even_hundred() {
(0..100, 200..300, 400..500)
.any()
.unify::()
.check(|value| assert!((value / 100) % 2 == 0));
}
fn main() {
// `checkito` comes with a bunch of builtin generators such as this generic
// number generator. An array of generators will produce an array of values.
let generator = [(); 10].map(|_| number::());
// For more configuration and control over the generation and shrinking
// processes, retrieve a [`check::Checker`] from any generator.
let mut checker = generator.checker();
checker.generate.count = 1_000_000;
checker.shrink.items = false;
// [`check::Checker::checks`] produces an iterator of [`check::Result`] which
// hold rich information about what happened during each check.
for result in checker.checks(|values| values.iter().sum::() < 1000.0) {
match result {
check::Result::Pass(_pass) => {}
check::Result::Shrink(_pass) => {}
check::Result::Shrunk(_fail) => {}
check::Result::Fail(_fail) => {}
}
}
// For simply sampling random values from a generator, use [`Sample::samples`].
// Just like in the checking process, samples will get increasingly larger.
for _sample in generator.samples(1000) {}
}
```
_See the [examples](examples/) and [tests](tests/) folder for more detailed examples._
---
### Contribute
- If you find a bug or have a feature request, please open an [issues](https://github.com/Magicolo/checkito/issues).
- `checkito` is actively maintained and [pull requests](https://github.com/Magicolo/checkito/pulls) are welcome.
- If `checkito` was useful to you, please consider leaving a [star](https://github.com/Magicolo/checkito)!
---
### Alternatives
- [proptest](https://crates.io/crates/proptest)
- [quickcheck](https://crates.io/crates/quickcheck)
- [arbitrary](https://crates.io/crates/arbitrary)
- [monkey_test](https://crates.io/crates/monkey_test)