| Crates.io | fn-fixture |
| lib.rs | fn-fixture |
| version | 1.0.2 |
| created_at | 2020-08-02 13:54:54.341309+00 |
| updated_at | 2021-02-07 17:26:42.933469+00 |
| description | Procedural macro designed to quickly generate snapshots of a fixture |
| homepage | https://github.com/Wolvereness/fn-fixture-rs/tree/main/fn-fixture |
| repository | https://github.com/Wolvereness/fn-fixture-rs/ |
| max_upload_size | |
| id | 272229 |
| size | 3,862 |
This crate provides an easy-to-use annotation for test creation. There are three aspects to each fixture:
A function that takes an input and has a return type that
implements std::fmt::Debug.
A folder of sub-folders (further nested as desired, it need not be
consistent) where every tree-terminating folder contains a single
input file (.rs .txt .bin) corresponding to the input type of
the function.
The expected results, generated automatically when not present. A panic can be considered a valid possible expected result.
This project follows convention not configuration. Input files are
simply named: input.rs input.txt input.bin. Expected-output
files are named by the name of the fixture. This means multiple
fixtures can share a folder-tree of tests. You are not intended to
make the output file yourself; one will be generated automatically
when an expected result is not present.
[dev-dependencies]
fn-fixture = "1.0.0"
This project uses itself to test itself, which triples as an example and a technical explanation.
snapshot-tests has four test-trees:
The source tree gives an explanation of
how tests get generated. This is also the primary means of testing
fn-fixture itself. These tests reference the other two test-trees.
The code tree gives an example of what
tests do when fed into the identity function. This is a tertiary
means of testing fn-fixture itself.
The bad tree gives examples of improperly
formed tests that result in compile-errors. These aren't tested
directly by fn-fixture, but rather the expected compile-failure
macroes are used in source/bads
The example tree follows the direct
example below.
self_snapshots.rs is the file that uses
fn-fixture to run these tests.
This example is part of the project, both for generation and
execution. To see the code generation in action, refer to
snapshot-tests/source/examples.
To see the file structure, refer to
snapshot-tests/examples.
This tests parsing a number from a text file:
#[fn_fixture::snapshot("snapshot-tests/examples")]
fn parse_unsigned_number(value: &str) -> Result<usize, impl std::fmt::Debug> {
value.parse()
}
#[fn_fixture::snapshot("snapshot-tests/examples")]
fn parse_signed_number(value: &str) -> Result<isize, impl std::fmt::Debug> {
value.parse()
}
snapshot-tests/examples/bad_number:
forty two
snapshot-tests/examples/good_number:
42
snapshot-tests/examples/sometimes_number:
-42
On first run, cargo test will have 6 tests (2 fixtures with 3
snapshots each) looking like this:
test parse_signed_number::sometimes_number ... FAILED
test parse_signed_number::good_number ... FAILED
test parse_unsigned_number::sometimes_number ... FAILED
test parse_unsigned_number::good_number ... FAILED
test parse_unsigned_number::bad_number ... FAILED
test parse_signed_number::bad_number ... FAILED
IntelliJ automatically structures the tests for you:

The specific errors will look like this:
thread 'parse_unsigned_number::bad_number' panicked at 'No expected value set: ...
Each snapshot folder, bad_number good_number and
sometimes_number, will now have both
parse_signed_number.actual.txt and
parse_unsigned_number.actual.txt.
The bad_number results will have:
Ok(
Err(
ParseIntError {
kind: InvalidDigit,
},
),
)
The good_number results will have:
Ok(
Ok(
42,
),
)
The sometimes_number will have the same results match for
parse_unsigned_number and parse_signed_number respectively.
Ok( represents that the thread did not
panic. If you expect a panic, then the outer-most should be Err(.Lastly, you review each .actual file manually. If the file is
correct, remove .actual. If not, continue to modify your code run
the tests; .actual will be overwritten with the results each run.
At some point, your output may change. For example, if one character
of shapshot-tests/examples/bad_number/parse_unsigned_number.txt
changes (like manually removing an i from the expected output), the
test result will look like this:
---- parse_unsigned_number::bad_number stdout ----
thread 'parse_unsigned_number::bad_number' panicked at 'assertion failed: `(left == right)`
left: `"Ok(\n Err(\n ParseIntError {\n kind: InvalidDigit,\n },\n ),\n)\n"`,
right: `"Ok(\n Err(\n ParseIntError {\n kind: InvaldDigit,\n },\n ),\n)\n"`', fn-fixture\tests\self_snapshots.rs:19:1
The generated code for this example will look like this (simplified/paraphrased to remove some edge-case handling and boilerplate):
fn parse_unsigned_number(value: &str, expected_file: &str) {
fn parse_unsigned_number(value: &str) -> Result<usize, impl std::fmt::Debug> {
value.parse()
}
// omitted logic for panic-handling
// omitted logic for writing actual file instead
assert_eq!(
format!("{:#?}\n", parse_unsigned_number(value)),
File::read_to_string(expected_file),
);
}
mod parse_unsigned_number {
#[test] fn bad_number() { super::parse_unsigned_number(include_str!("snapshot-tests/examples/bad_number/input.txt"), "snapshot-tests/examples/bad_number/parse_unsigned_number.txt") }
#[test] fn good_number() { /* ... */ }
#[test] fn sometimes_number() { /* ... */ }
}
fn parse_signed_number(value: &str, expected_file: &str) {
fn parse_signed_number(value: &str) -> Result<isize, impl std::fmt::Debug> {
value.parse()
}
// omitted logic for panic-handling
// omitted logic for writing actual file instead
assert_eq!(
format!("{:#?}\n", parse_signed_number(value)),
File::read_to_string(expected_file),
);
}
mod parse_signed_number {
#[test] fn bad_number() { super::parse_signed_number(include_str!("snapshot-tests/examples/bad_number/input.txt"), "snapshot-tests/examples/bad_number/parse_signed_number.txt") }
#[test] fn good_number() { /* ... */ }
#[test] fn sometimes_number() { /* ... */ }
}
input.txt corresponds to include_str!(...)input.bin corresponds to include_bytes!(...)input.rs corresponds to include!(...)
input.rs can be literally anything that fits into
the single-parameter of the call to the fixture.
snapshot-tests/code has plenty of examples
of using a rust code as input.The name of the fixture may not be input. That would mean the
expected output is input.txt; call it quine instead.
Expected-panics should be String or &str. This should rarely,
if ever, be an issue. Every known library uses those.
panic!("At the disco") for example is a &str and
unwrap()/expect(...) use String.
Multiline string output should be wrapped in a
.lines().collect::<Vec<String>>(). These tests are for humans to
review. IntelliJ will diff it for you.
Every terminating directory (one without sub-directories) must have
exactly one input file.
A directory with sub-directories may not have an input file.
The referenced folder is a top-level, not a test itself.
Adding new tests without touching the including file or clearing the compiler cache will be ignored. This is a compiler-level restriction.
Every folder in a tree must be a valid rust identifier. These are how nested test modules are named.
Adding other files into the folder is discouraged, and future versions may treat them as an error.
Return type must implement std::fmt::Debug.
Annotating a field is unsupported, even if it's callable.
Use impl std::fmt::Debug as the return type.
Use a different function for different parts of the output; don't have one function parse input two different ways.
Although this library supports panics as expected output, testing that behavior is discouraged.
Name your directory snapshot-tests. Snapshot explains what kind
of test, and IDEs may recognize a directory ending in -tests.
Generics work for multiple input.rs files of different types.
This application is derived from an internally developed tool, thus released under the MIT License: