rvtest 0.1.0

A Next Level Testing Framework for Rust — BDD specs, property-based testing, parametrized tests, rich reporting, and code coverage
Documentation
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use std::time::Duration;

use rvtest::core::{RunnerConfig, TestRun};
use rvtest::param::{parametrize, parametrize_named};
use rvtest::property::{any, check};
use rvtest::report::{CompactReporter, JsonReporter, PrettyReporter, TapReporter, TestReporter};
use rvtest::runner::TestRunner;
use rvtest::spec::describe;

#[test]
fn rvtest_spec() {
    describe("Spec")
        .describe("execution")
            .it("passes when all tests pass", || {
                describe("Math")
                    .it("adds", || assert_eq!(2 + 2, 4))
                    .it("subtracts", || assert_eq!(5 - 3, 2))
                    .run()
                    .assert_all_pass();
            })
            .it("reports failures", || {
                let result = catch_unwind(AssertUnwindSafe(|| {
                    describe("Failing")
                        .it("fails", || panic!("intentional failure"))
                        .run()
                        .assert_all_pass();
                }));
                assert!(result.is_err(), "assert_all_pass should panic on failure");
            })
            .it("supports tags and timeout", || {
                describe("Tagged")
                    .it("passing", || {})
                    .tag("smoke")
                    .timeout(Duration::from_secs(1))
                    .run()
                    .assert_all_pass();
            })
            .it("retries flaky tests", || {
                let counter = AtomicU32::new(0);
                describe("Flaky")
                    .it("succeeds on retry", move || {
                        let prev = counter.fetch_add(1, Ordering::SeqCst);
                        if prev == 0 {
                            panic!("first attempt fails");
                        }
                    })
                    .retries(2)
                    .run()
                    .assert_all_pass();
            })
            .it("runs before_all hook", || {
                let ran = Arc::new(AtomicBool::new(false));
                let setup = Arc::clone(&ran);
                describe("Setup")
                    .before_all(move || {
                        setup.store(true, Ordering::SeqCst);
                    })
                    .it("hook executed", move || {
                        assert!(ran.load(Ordering::SeqCst), "before_all should have run");
                    })
                    .run()
                    .assert_all_pass();
            })
        .tag("spec")
        .run()
        .assert_all_pass();
}

#[test]
fn rvtest_parametrized() {
    describe("Parametrized")
        .it("runs all cases", || {
            let results = parametrize("add", [(1, 1, 2), (0, 0, 0), (-1, 1, 0)], |(a, b, exp)| {
                assert_eq!(a + b, *exp);
            });
            assert!(results.iter().all(|c| c.status.is_passed()));
            assert_eq!(results.len(), 3);
        })
        .it("supports named cases", || {
            let results = parametrize_named(
                "parse",
                [("empty", ""), ("valid", "42")],
                |input| {
                    if !input.is_empty() {
                        assert!(input.parse::<i32>().is_ok());
                    }
                },
            );
            assert!(results.iter().all(|c| c.status.is_passed()));
        })
        .tag("param")
        .run()
        .assert_all_pass();
}

#[test]
fn rvtest_property() {
    describe("Property")
        .it("passes for valid properties", || {
            check(
                "identity with zero",
                any::<i32>(),
                |a: &i32| a + 0 == *a,
            );
        })
        .it("detects falsified properties", || {
            let result = catch_unwind(AssertUnwindSafe(|| {
                check(
                    "intentionally false",
                    any::<i32>(),
                    |_: &i32| false,
                );
            }));
            assert!(result.is_err(), "check should panic on falsified property");
        })
        .tag("property")
        .run()
        .assert_all_pass();
}

#[test]
fn rvtest_runner() {
    describe("Runner")
        .it("executes specs with custom config", || {
            let config = RunnerConfig {
                parallel: false,
                verbose: true,
                ..RunnerConfig::default()
            };

            let run = TestRunner::new(config)
                .add_spec(describe("Runner test").it("works", || {}))
                .run();

            assert!(run.success());
            assert_eq!(run.total(), 1);
        })
        .tag("runner")
        .run()
        .assert_all_pass();
}

#[test]
fn rvtest_reporters() {
    describe("Reporters")
        .it("pretty reporter shows summary", || {
            let report = PrettyReporter::new(false).colour(false).report(&TestRun::new());
            assert!(report.contains("0 passed"), "should show pass count");
            assert!(report.contains("0 failed"), "should show fail count");
        })
        .it("tap reporter outputs correct header", || {
            let report = TapReporter.report(&TestRun::new());
            assert!(report.starts_with("1..0"));
        })
        .it("compact reporter shows counts", || {
            let report = CompactReporter.report(&TestRun::new());
            assert!(report.contains("Results:"), "should have results line: {report:?}");
            assert!(report.contains("0/0"), "should show zero counts");
        })
        .it("json reporter is valid", || {
            let report = JsonReporter.report(&TestRun::new());
            assert!(report.contains(r#""success":true"#));
            assert!(report.contains(r#""suites":["#));
        })
        .tag("report")
        .run()
        .assert_all_pass();
}