Skip to content

rtb-test-support

rtb-test-support builds an App for a test without the full rtb-cli lifecycle — no logging setup, no miette hook install, no signal handlers. It is a separate crate so that depending on it is a visible statement of intent in a Cargo.toml.

[dev-dependencies]
rtb-test-support = "0.9"

It shares a version line with rtb-app and is released at the same time, so the two version numbers always match.

TestWitness

pub struct TestWitness(());

impl TestWitness {
    pub const fn new() -> Self;
}

impl Default for TestWitness { /* delegates to new() */ }

A zero-sized value proving the caller depends on rtb-test-support. It implements a crate-private Sealed trait, and TestAppBuilder accepts only sealed types — so a crate that depends on rtb-app alone cannot reach the builder.

The seal is a visibility signal, not access control. See Why test construction is sealed but not locked.

TestAppBuilder

#[must_use]
pub struct TestAppBuilder<W: sealed::Sealed> { /* … */ }

impl TestAppBuilder<TestWitness> {
    pub const fn new(witness: TestWitness) -> Self;
    pub fn tool(self, name: &str, version: &str) -> Self;
    pub fn metadata(self, m: ToolMetadata) -> Self;
    pub fn version(self, v: VersionInfo) -> Self;
    pub fn config<C>(self, config: Config<C>) -> Self;
    pub fn config_value<C>(self, c: C) -> Self;
    pub fn build(self) -> App;
}

tool — the shortcut, and what it also sets

let app = TestAppBuilder::new(TestWitness::new()).tool("mytool", "1.2.3").build();

tool sets both metadata and version. The metadata it builds has name as given and summary fixed to the literal "test"; everything else is at its default. The version string must parse as semver — it panics with parse test version if it does not, which is acceptable in a test and would not be anywhere else.

Because tool writes both fields, ordering matters: calling .metadata(m) and then .tool(...) throws the metadata away. Put tool first, or use metadata and version and skip it.

metadata and version — the explicit route

let app = TestAppBuilder::new(TestWitness::new())
    .metadata(ToolMetadata::builder().name("mytool").summary("real summary").build())
    .version(rtb_app::version_info!())
    .build();

Use these when the test cares about a metadata field tool cannot reach — a release_source, a help channel, an update_policy.

config and config_value — wiring typed configuration

pub fn config<C>(self, config: Config<C>) -> Self
where C: Serialize + DeserializeOwned + JsonSchema + Send + Sync + 'static;

pub fn config_value<C>(self, c: C) -> Self
where C: Serialize + DeserializeOwned + JsonSchema + Send + Sync + 'static;
Method Use it when Equivalent to
config_value(c) the test only cares about one merged value config(Config::<C>::with_value(c))
config(cfg) the test needs layered defaults and overrides the production Application::builder().config(...)

Either one attaches the TypedConfigOps bundle, so after calling one of them App::config_schema() and App::config_value() both return Some. Call neither and both return None, and typed_config::<C>() resolves only for C = ().

Note the bounds are stricter than App::new's: JsonSchema and Serialize are required here because the schema and the rendered value are generated eagerly.

build — and what it panics on

pub fn build(self) -> App;
Missing Panic message
metadata (no tool, no metadata) TestAppBuilder: metadata not set
version (no tool, no version) TestAppBuilder: version not set

Panicking rather than returning Result is deliberate: a test that forgot to say which tool it is testing should stop, loudly.

What TestAppBuilder cannot set

These are fixed, with no builder method to change them:

Part of App What you get Workaround
assets Assets::default() none through the builder
shutdown a fresh root CancellationToken none — derive children with child_token() after build()
credentials_provider None assign it after build(); the field is public
trailing_args empty call App::with_trailing_args(...) on the built App
let mut app = TestAppBuilder::new(TestWitness::new()).tool("t", "1.0.0").build();
app.credentials_provider = Some(std::sync::Arc::new(my_config));

Relationship to App::for_testing

rtb_app::App::for_testing is a #[doc(hidden)] pub fn used by rtb-app's own tests. It is not cfg(test)-gated and there is no Cargo feature guarding it, so any crate depending on rtb-app can call it. New downstream tests should use TestAppBuilder — it is the promoted path, it can wire typed config, and its signature makes test-only intent obvious.