Crate srctrait_common_testing

Source
Expand description

§SrcTrait Common Testing

Structured testing with setup, teardown, and standardized fixture and temp directories

§Features

  • Structure tests into inheritable heirarchies: Module -> Test
  • Bundle common assets outside of heirarchies using Group
  • Setup and teardown callbacks for each Module, Test, and Group
  • Module and Group setup occurs before Test
  • Categorize based on use-case: Unit and Integration
  • Standardized paths for fixture and temp directories
  • Miscellaneous helper functions for common tasks

§Restrictions

  • One Module per Rust test module
  • One Test per Rust test function
  • Module and Group are static to a Rust module
  • Test is instantiated non-static inside of the Rust test function
  • Unit tests live in src
  • Integration tests live in tests or example
  • Fixture asset files live in testing

§Standard paths

use-case / module_path / function_name

See the section on namepaths for details.

§Example

pub fn main(){}

// Just something to test against
pub fn fibonacci(n: u8) -> Option<u128> {
    if n == 0 {
        return Some(0);
    } else if n == 1 {
        return Some(1);
    } else if n > FIBONACCI_MAX_N_U128 { // overflow u128
        return None;
    }

    let mut sum: u128 = 0;
    let mut last: u128 = 0;
    let mut curr: u128 = 1;
    for _i in 1..n {
        if let Some(r) = last.checked_add(curr) {
            sum = r;
        } else {
            return None;
        }

        last = curr;
        curr = sum;
    }

    Some(sum)
}

const FIBONACCI_MAX_N_U128: u8 = 185;

// Testing namepaths will strip '::tests' out.
#[cfg(test)]
mod tests {
    use std::{fs, sync::LazyLock};
    use super::*;
    use srctrait_common_testing::prelude::*;

    // Module setup runs before any tests that use the module.
    // Teardown runs on process exit.
    //
    // Fixture dirs are enforced at runtime and panic if they don't exist.
    //
    // This module's namepath().full_path() will be:
    //     srctrait-common-testing/integration/example-fibonacci
    //
    // This module's namepath().path() will be:
    //     integration/example-fibonacci
    //
    // fixture dir: $CRATE/testing/fixtures/integration/example-fibonacci
    // temp dir: $TMP/srctrait-common-testing-example-fibonacci.XXXXXXXX
    static TESTING: testing::Module = testing::module!(Integration, {
            .using_fixture_dir()
            .using_temp_dir()
            .teardown_static(teardown)
            .setup(|module| {
                let fibonacci_u128_csv = (0..FIBONACCI_MAX_N_U128)
                    .map(|n| fibonacci(n).unwrap().to_string())
                    .collect::<Vec<_>>()
                    .join(",");

                let fibonacci_csv_tmp_file = module.temp_dir()
                    .join(&*FIBONACCI_U128_CSV_FILENAME);

                fs::write(&fibonacci_csv_tmp_file, fibonacci_u128_csv).unwrap();
            })
    });

    static FIBONACCI_U128_CSV_FILENAME: LazyLock<String> = LazyLock::new(||
        "fibonacci-u128.csv".to_string());

    // Anything const or static can be used with static teardown functions,
    // including things like LazyLock.
    //
    // Module's internal teardown will delete its temp directory before
    // running its custom teardown.
    extern "C" fn teardown() {
        println!("Farewell, Fibonacci. Don't worry, {} has already been deleted.",
            *FIBONACCI_U128_CSV_FILENAME);
    }

    // Groups are standalone and work similar to Modules, but without children.
    // They have their own tmp and fixture directories, setup, and teardown.
    //
    // This group's namepath().full_path() will be:
    //     srctrait-common-testing/integration/example-fibonacci/100
    // This group's namepath().path() will be:
    //     integration/example-fibonacci/100
    static GROUP_100: testing::Group = testing::group!("example-fibonacci/100", Integration, {
        .using_fixture_dir()
    });

    // The #[tested] helper is merely the equivalent of #[test] and #[named]
    #[tested]
    fn test_u128() {
        // test!() assumes a module named `TESTING` is in scope and links to it.
        //
        // The "inherit_" methods use the same dir as their parent module.
        // The "using_" methods will append a subdir named for the test function.
        //
        // Tests block on their module's setup. The tmp file needed here will exist.
        //
        // fixture dir: $CRATE/testing/fixtures/integration/example-fibonacci/test-u128
        // tmp dir: $TMP/srctrait-common-testing-example-fibonacci.XXXXXXXX
        let test = testing::test!({
            .using_fixture_dir()
            .inherit_temp_dir()
        });

        let fibonacci_fixture_csv_file = test.fixture_dir().join(&*FIBONACCI_U128_CSV_FILENAME);
        let expected_fibonacci_u128 = fs::read_to_string(fibonacci_fixture_csv_file).unwrap()
            .split(",")
            .map(|d| d.trim().parse().unwrap())
            .collect::<Vec<u128>>();

        let fibonacci_tmp_csv_file = test.temp_dir().join(&*FIBONACCI_U128_CSV_FILENAME);
        let actual_fibonacci_u128 = fs::read_to_string(fibonacci_tmp_csv_file).unwrap()
            .split(",")
            .map(|i| i.parse().unwrap())
            .collect::<Vec<u128>>();

        assert_eq!(expected_fibonacci_u128, actual_fibonacci_u128);
    }

    // Demonstration using a Group
    #[tested]
    fn test_100() {
        // This empty Test will block on its Module's setup.
        let _test = testing::test!();

        // Groups will block on their setup.
        let fib100_file = GROUP_100.fixture_dir().join("fib-100.txt");

        let fib100: u128 = fs::read_to_string(fib100_file)
            .map(|d| d.trim().parse().unwrap())
            .unwrap();

        assert_eq!(fib100, fibonacci(100).unwrap());
    }
}

§Namepaths

The crate name and ::tests are stripped from paths.

§Format

Moduleuse-case / module_path ..
Testuse_case / module_path .. / function_name
Groupuse_case / arbitrary_path ..

§Examples

ModuleUnitmy-crate::process::numbers::tests
unit/process/numbers
ModuleIntegrationtest-module::tests
integration/test-module
TestUnitmy-crate::process::numbers::tests::test_func_one()
unit/process/numbers/test_func_one
TestIntegrationtest-module::tests::test_func_two()
integration/test-module/test_func_two
GroupUnitmy-crate::process::numbers::tests “primes/large”
unit/primes/large
GroupIntegrationtest-module::tests “primes/large”
integration/primes/large

Re-exports§

pub use srctrait_common_tooling as tooling;

Modules§

prelude

Macros§

group
Constructs a TestGroup and wraps it in Group
module
Constructs a TestModule and wraps it inside a lazy-locked Module
test
Constructs a Test using with a parent TestModule in scope named, “TESTING”.
test_with
Constructs a Test using a custom ident for its parent TestModule.

Structs§

Group
Lazy-locked wrapper for TestGroup
GroupBuilder
Constructs a TestGroup
Module
Lazy-locked wrapper for TestModule.
ModuleBuilder
Builds a new TestModule
Namepath
Describes the normalized namepath of a TestModule, TestGroup, or Test.
RawNamepath
Retains the original elements used to construct a Namepath
Test
Configuraiton for a single unit or integration test.
TestBuilder
Constructs a Test
TestGroup
Standalone top-level testing group.
TestModule
Represents a Rust module that contains tests.

Enums§

TestingKind
The type of testing model
UseCase
The type of testing being performed

Traits§

Testing
Common to all testing models: TestModule, TestGroup, and Test.