snapbox Snapshot Testing Toolbox

repository·main·Indexed 41 days ago

https://github.com/assert-rs/snapbox

A snapshot-testing toolbox for Rust designed to verify function return values, CLI stdout/stderr, and filesystem changes. It includes the `trycmd` test harness for verifying CLI applications using external files (such as .toml or Markdown) and provides a `Command` API to spawn processes and assert their exit status and output.

Tokens
12.1K
Snippets
43
Records
56
Agent score
51%

What's inside snapbox

  1. Overview of snapbox snapshot testing

    main

    snapbox is a snapshot-testing toolbox designed to verify various types of output during testing. It is useful for ensuring consistency in:

    • Function return values: Verifying the data returned by your Rust functions.
    • CLI stdout/stderr: Capturing and comparing command-line interface output.
    • Filesystem changes: Verifying that files are created, modified, or deleted as expected.

    It is built to be flexible enough to serve as a foundation for custom test harnesses (similar to trycmd).

  2. Use variables for runtime output replacement in .trycmd

    main

    When testing commands that produce dynamic or runtime-generated output, you can use replacement variables to ensure tests remain deterministic.

    Define a variable (e.g., [REPLACEMENT]) in your test registration. The test runner will replace occurrences of that variable in the expected output block with the actual runtime value. Note that the actual runtime value can still appear in the test block itself; the variable is used for matching purposes.

    $ simple "blah blah runtime-value blah"
    Hello blah blah [REPLACEMENT] blah!
  3. Use the .trycmd file format for CLI testing

    main

    The .trycmd format allows you to test CLI binaries by writing expected command invocations and their outputs directly in Markdown files. The test runner looks for code blocks tagged with either console or trycmd languages.

    To define a test case, use a fenced code block where each line starts with a $ followed by the command to execute, and subsequent lines represent the expected output.

    $ simple World
    Hello World!
  4. Test command failures and environment variables in .trycmd

    main

    You can verify that a command fails by using the ? prefix followed by the expected exit code. Additionally, you can pass environment variables inline before the command.

    • Failures: Use ? <exit_code> to assert a specific exit status.
    • Environment Variables: Prefix the command with KEY=VALUE.
    $ simple
    ? 1
    Must supply exactly one argument.
    
    $ GOODBYE=true simple World
    Goodbye World!
  5. Set up a minimal trycmd test suite

    main

    To set up trycmd in your project:

    1. Create an integration test file (e.g., tests/cli_tests.rs).
    2. Implement a test function using trycmd::TestCases::new().
    3. Chain .case(pattern) calls to point to your test files (e.g., .toml files) or documentation files (e.g., README.md).
    4. Run the tests using the standard cargo test command.

    TestCases will automatically enumerate all files matching the provided patterns and execute the commands contained within them, verifying the output.

    #[test]
    fn cli_tests() {
        trycmd::TestCases::new()
            .case("tests/cmd/*.toml")
            .case("README.md");
    }
  6. Use trycmd to run CLI test cases

    main

    trycmd is a test harness designed to verify CLI tools by enumerating test case files and running them to ensure the results match expectations. It is inspired by trybuild and cram.

    To use it, you define a test function in your Rust integration tests that uses trycmd::TestCases to specify the patterns or files containing your test cases. You can use .toml files for structured test cases or use existing documentation files like README.md as test sources.

    #[test]
    fn cli_tests() {
        trycmd::TestCases::new()
            .case("tests/cmd/*.toml")
            .case("README.md");
    }
  7. Use filters to normalize snapshots

    main

    Snapbox provides a Filter trait to transform actual or expected Data before comparison. This is useful for making snapshots consistent across different platforms (e.g., normalizing line endings or path separators) or focusing on specific data characteristics while ignoring volatile information.

    To use a filter, you implement or use a type that satisfies the Filter trait, which provides a filter(&self, data: Data) -> Data method.

    // Example of the Filter trait interface
    impl Filter for MyCustomFilter {
        fn filter(&self, data: Data) -> Data {
            // Return transformed Data
            data
        }
    }
  8. How snapshot actions work

    main

    The Action determines what happens when an assertion fails. You can control this via the action() method or by setting an environment variable via action_env().

    ActionBehavior
    SkipDoes nothing; the assertion always passes.
    IgnorePrints a warning to stderr but does not fail the test.
    VerifyThe default behavior. Fails the test and prints a detailed diff. If an environment variable was configured via action_env, it also prints a hint on how to overwrite the snapshot.
    OverwriteAttempts to write the actual value into the expected source (e.g., updating the snapshot file). If successful, it prints a 'Fixing' warning; if it fails, it panics.
  9. Snapshot types: `str!` and `file!`

    main

    Snapbox provides two primary ways to define snapshots:

    • str!: Used for in-source snapshots. The snapshot content is embedded directly within your Rust source code.
    • file!: Used for external snapshots. The snapshot content is stored in separate files outside the source code.

    These are used as arguments within macros like assert_data_eq!.

  10. How built-in redactions work in Snapbox

    main

    Snapbox supports built-in redaction patterns within your expected data to allow for flexible matching. These redactions are inferred from the expected pattern and applied to the actual data during normalization:

    1. Multi-line elision: Using ... on a line by itself matches any number of complete lines in the actual data.
    2. In-line character elision: Using [..] within a line matches multiple characters within that line.

    Example patterns:

    • hello [..] world matches hello beautiful world.
    • ... next line matches any number of lines followed by next line.
  11. Handle conditional redactions with clear_unused

    main

    When using redactions that might not always appear in the output (for example, a platform-specific file extension), you should use clear_unused on your expected pattern. This method removes the placeholder from the expected string if the value it represents was not found, preventing false negatives in your assertions.

    let mut subst = snapbox::Redactions::new();
    subst.insert("[EXE]", std::env::consts::EXE_SUFFIX);
    
    // If the actual output does NOT contain the EXE_SUFFIX,
    // clear_unused will strip '[EXE]' from the pattern so it matches.
    let pattern = "my_binary[EXE]";
    let cleaned_pattern = subst.clear_unused(pattern);
    // cleaned_pattern might be "my_binary" if [EXE] wasn't found
  12. How unordered matching works

    main

    When .unordered() is enabled on a NormalizeToExpected instance, Snapbox attempts to make content comparable regardless of element order. The logic re-orders the actual data to follow the sequence defined in the expected data.

    • Text: Lines in the actual text are re-ordered to match the sequence of lines in the expected text.
    • JSON Arrays: Elements in the actual array are re-ordered to match the sequence in the expected array.
    • JSON Objects: Keys are processed based on the expected object structure.
    • Term-SVG: The body of the SVG is normalized using unordered text matching while preserving the header and footer.