Use assets in tests¶
You want tests that cover the code which reads assets, without embedding fixtures in the binary or writing files to disk.
Accept an Assets, do not construct one¶
The single change that makes asset-reading code testable is taking the overlay as a parameter rather than building it inside the function:
// Testable: the caller decides where assets come from.
fn render_template(assets: &Assets, name: &str) -> Result<String, AssetError> {
assets.open_text(&format!("templates/{name}.md"))
}
Assets is Clone at refcount cost and Send + Sync, so passing it
around — into threads, into async tasks, into every command — is cheap.
Production wires the real stack once at startup; tests wire whatever
they need.
Build the fixture stack in the test¶
use std::collections::HashMap;
use rtb_assets::Assets;
fn fixtures(files: &[(&str, &str)]) -> HashMap<String, Vec<u8>> {
files
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).as_bytes().to_vec()))
.collect()
}
#[test]
fn renders_the_readme_template() {
let assets = Assets::builder()
.memory("fixtures", fixtures(&[("templates/readme.md", "# {{ name }}")]))
.build();
assert_eq!(render_template(&assets, "readme").unwrap(), "# {{ name }}");
}
Keys are matched literally, so write them exactly as production code
asks for them — no leading ./, no leading /.
Test precedence with two memory layers¶
Layer ordering is the behaviour most worth a test, and it needs no disk at all. Register lowest priority first:
#[test]
fn user_layer_wins_over_defaults() {
let assets = Assets::builder()
.memory("defaults", fixtures(&[("greeting.txt", "hello")]))
.memory("user", fixtures(&[("greeting.txt", "hi")]))
.build();
assert_eq!(assets.open_text("greeting.txt").unwrap(), "hi");
}
The same trick covers merge behaviour: put the shipped YAML in the lower layer and the override in the upper, and assert on the deserialised result.
Start from Assets::default() when the code should find nothing¶
Assets::default() is a valid overlay with no layers. Every read
returns nothing, no read errors, and no builder call is needed:
#[test]
fn reports_a_missing_template() {
let err = render_template(&Assets::default(), "nope").unwrap_err();
assert!(matches!(err, AssetError::NotFound(_)));
}
Use a real directory only when the directory is the thing under test¶
MemorySource cannot exercise the path-traversal rules, permission
handling, or a missing root — those live in DirectorySource. For
those, use tempfile:
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("assets")).unwrap();
std::fs::write(dir.path().join("assets/allowed.txt"), b"ok").unwrap();
let assets = Assets::builder()
.directory(dir.path().join("assets"), "disk")
.build();
assert_eq!(assets.open("../secret.txt"), None);
Keep the tempdir alive for the duration of the test — dropping it deletes the directory, and the layer then quietly returns nothing rather than failing loudly.
Do not embed test fixtures into the library¶
#[derive(RustEmbed)] on a test folder works and this crate's own
suite does it to cover EmbeddedSource. For anything else it is the
wrong tool: the folder must exist at compile time, changes need a
rebuild to be seen (debug-embed is enabled, so there is no
read-from-disk fallback in debug builds), and the fixtures end up in
the built artefact.
Reach for MemorySource unless the thing you are testing is embedding
itself.
Related¶
- Asset source reference — what each built-in layer will and will not do.
- Diagnose a failed asset lookup — when the fixture is there and the read still returns nothing.