rstest

repository·master·Indexed 23 days ago

https://github.com/la10736/rstest

A fixture-based testing framework for Rust that provides tools for writing parameterized and table-based tests using procedural macros. It features attributes for fixtures, cases, values, and file-based test generation, as well as support for async tests, timeouts, and singleton fixtures. The ecosystem includes rstest_reuse for sharing test templates across functions and crates, and rstest_fixtures for managing resource cleanup via the TearDown trait.

Tokens
8.5K
Snippets
36
Records
45
Agent score
81%

What's inside rstest

  1. Use Magic Conversion for FromStr types

    master

    If a type implements the FromStr trait, you can pass string literals in #[case] or #[values] attributes, and rstest will automatically convert them to the target type.

    // Example: Converting string literals to SocketAddr
    #[rstest]
    #[case("1.2.3.4:8080", 8080)]
    #[case("127.0.0.1:9000", 9000)]
    fn check_port(#[case] addr: SocketAddr, #[case] expected: u16) {
        assert_eq!(expected, addr.port());
    }
  2. Inject and override fixture arguments

    master

    Fixtures can be injected by other fixtures. You can customize a fixture's behavior in a specific test using:

    • #[default(value)]: Provides a default value for a fixture argument if none is provided.
    • #[with(arg1, arg2, ...)]: Overrides the fixture's arguments with specific values for that test case.

    Example of a fixture with default values and overriding them in a test:

    #[fixture]
    fn user(#[default("Alice")] name: &str, #[default(22)] age: u8) -> User {
        User::new(name, age)
    }
    
    #[rstest]
    fn is_alice(user: User) {
        assert_eq!(user.name(), "Alice")
    }
    
    #[rstest]
    fn is_bob(#[with("Bob")] user: User) {
        assert_eq!(user.name(), "Bob")
    }
    
    #[rstest]
    fn is_42(#[with("", 42)] user: User) {
        assert_eq!(user.age(), 42)
    }
  3. Compose templates with additional cases and values

    master

    You can extend a template by adding more #[case] or #[values] attributes when applying it. This allows you to combine a base set of parameters with additional specific scenarios.

    • Adding cases: Use #[case(...)] alongside #[apply(...)] to add new rows to the existing template's test matrix.
    • Adding values: Use #[values(...)] to provide a list of values for a new argument in the target function.
    • Template for values: Templates can also be used to define #[with(...)] (fixtures) or #[values(...)] lists that are then applied to other tests.
    #[template]
    #[rstest]
    #[case(2, 2)]
    #[case(4/2, 2)]
    fn base(#[case] a: u32, #[case] b: u32) {}
    
    // Add a new case and a new argument via #[values]
    #[apply(base)]
    #[case(9/3, 3)]
    fn it_works(a: u32, b: u32, #[values("a", "b")] t: &str) {
        assert!(a == b);
        assert!("abcd".contains(t))
    }
  4. How fixtures work in rstest

    master

    Fixtures allow you to inject dependencies into tests by passing them as arguments. You define a fixture using the #[fixture] attribute. When a test function includes the fixture's name as an argument, rstest automatically provides the value returned by the fixture function.

    use rstest::*;
    
    #[fixture]
    pub fn fixture() -> u32 { 42 }
    
    #[rstest]
    fn should_success(fixture: u32) {
        assert_eq!(fixture, 42);
    }
  5. Export templates across crates using `#[export]`

    master

    To make a template available to other crates, use the #[export] attribute.

    When exporting templates, you must follow these two steps to ensure visibility:

    1. In the exporting crate: Declare rstest_reuse as pub at the crate root (e.g., in lib.rs or main.rs) so that importing crates can access the macros.
    2. In the importing crate: Import the template normally using pub use or similar.

    Example of declaring it in the exporting crate root:

    #[cfg(test)]
    pub use rstest_reuse;
  6. Reuse parametrized tests with `#[template]` and `#[apply]`

    master

    The rstest_reuse crate allows you to define a set of test cases once as a template and apply them to multiple test functions. This avoids duplicating #[case] definitions or writing complex macros.

    • Use #[template] on a function to define a reusable test pattern. This function must also be annotated with #[rstest].
    • Use #[apply(template_name)] on a target function to inject the template's cases into it.

    If the argument names in your target function match the names in the template, you do not need to repeat the #[case] attributes on the arguments.

    use rstest::rstest;
    use rstest_reuse::{self, *};
    
    // Define the template
    #[template]
    #[rstest]
    #[case(2, 2)]
    #[case(4/2, 2)]
    fn two_simple_cases(#[case] a: u32, #[case] b: u32) {}
    
    // Apply the template to a test function
    #[apply(two_simple_cases)]
    fn it_works(a: u32, b: u32) {
        assert!(a == b);
    }
  7. Reuse test templates with rstest_reuse

    master

    To use the same set of parameterized test cases across multiple test functions, use the rstest_reuse crate. Define a template using #[template] and #[rstest], then apply it to other tests using the #[apply(template_name)] attribute.

    use rstest::rstest;
    use rstest_reuse::{self, *};
    
    #[template]
    #[rstest]
    #[case(2, 2)]
    #[case(4/2, 2)]
    fn two_simple_cases(#[case] a: u32, #[case] b: u32) {}
    
    #[apply(two_simple_cases)]
    fn it_works(#[case] a: u32, #[case] b: u32) {
        assert!(a == b);
    }
  8. Handle async tests and futures

    master

    rstest supports async tests but does not provide a runtime. You must use an async test attribute (like #[tokio::test] or #[async_std::test]).

    To simplify working with async inputs (fixtures or parameters), use the #[future] attribute. This allows you to use the underlying type T instead of impl Future<Output = T>.

    To avoid manual .await calls on every future input, you can use:

    • #[awt] on the test function to globally .await all #[future] inputs.
    • #[future(awt)] on a specific argument to await just that one.
    use rstest::*;
    
    #[fixture]
    async fn base() -> u32 { 42 }
    
    #[rstest]
    #[tokio::test]
    #[case(21, async { 2 })]
    #[case(6, async { 7 })]
    #[awt]
    async fn global(
        #[future] base: u32,
        #[case] expected: u32,
        #[future] #[case] div: u32
    ) {
        assert_eq!(expected, base / div);
    }
  9. Configure explicit and implicit test attributes

    master

    When using non-standard test runners (like smol or actix), you can specify how rstest interacts with them:

    1. Implicit Test Attributes: If an attribute's path ends in test (e.g., #[actix_rt::test]), rstest treats it as the test runner. This replaces the default #[test].
    2. Explicit Test Attributes: Use #[test_attr(attribute_syntax)] to pass arbitrary attributes to the test. This is useful for complex macros like apply(test).

    Note: Always place these attributes after any #[case()] attributes to ensure they bind to the test function itself.

    // Explicit example
    #[rstest]
    #[case(2, async { 4 })]
    #[test_attr(apply(test))]
    async fn my_async_test(#[case] a: u32, #[case] #[future] result: u32) {
        assert_eq!(2 * a, result.await);
    }
  10. What is rstest and how to use it

    master

    rstest is a fixture-based testing framework for Rust. It allows you to encapsulate test dependencies into reusable functions called fixtures. Instead of manually setting up objects (like users or databases) inside every test, you declare them as arguments to your test functions, and rstest handles the instantiation.

    Key features include:

    • Fixtures: Reusable dependency injection via the #[fixture] macro.
    • Parametrized Tests: Running the same test with different inputs using #[case].
    • Combinatorial Testing: Generating tests for every combination of provided values using #[values].
    • Magic Conversion: Automatically converting string literals to types that implement FromStr (e.g., SocketAddr).
  11. Handle async tests and futures with `#[future]`

    master

    If your test function is async, rstest will run all cases as async tests. If your test inputs (fixtures or cases) are Future types, you can use the #[future] attribute to automatically handle the boilerplate of awaiting them.

    • #[future]: Marks an argument as a future that should be awaited.
    • #[future(awt)]: Averages the input (awaits it) within the argument expression.
    • #[awt]: A function-level attribute that globally .awaits all #[future] inputs in that test.
    use rstest::*;
    
    #[fixture]
    async fn base() -> u32 { 42 }
    
    #[rstest]
    #[case(21, async { 2 })]
    #[tokio::test]
    #[awt]
    async fn global(#[future] base: u32, #[case] expected: u32, #[future] #[case] div: u32) {
        assert_eq!(expected, base / div);
    }