Build an App in a test¶
Add the dependency¶
[dev-dependencies], not [dependencies]. A production binary that only depends on
rtb-app cannot reach TestAppBuilder at all, and keeping it here is what makes
that true.
Build the simplest possible App¶
use rtb_test_support::{TestAppBuilder, TestWitness};
#[test]
fn app_has_the_tool_name() {
let app = TestAppBuilder::new(TestWitness::new())
.tool("mytool", "1.2.3")
.build();
assert_eq!(app.metadata.name, "mytool");
assert_eq!(app.version.version.major, 1);
assert!(!app.shutdown.is_cancelled());
}
tool sets name and version together. It also sets summary to the literal
"test", so do not assert on the summary after using it. If the test cares about
other metadata, build the metadata yourself:
let app = TestAppBuilder::new(TestWitness::new())
.metadata(
ToolMetadata::builder()
.name("mytool")
.summary("does the thing")
.update_policy(UpdatePolicy::Prompt)
.build(),
)
.version(rtb_app::version_info!())
.build();
Call tool before metadata if you use both — tool overwrites metadata and
version, so calling it second throws away what you just set.
Drive a command¶
Command::run is async, so the test needs a runtime:
#[tokio::test]
async fn deploy_succeeds() {
let app = TestAppBuilder::new(TestWitness::new()).tool("mytool", "1.0.0").build();
Deploy.run(app).await.expect("deploy should succeed");
}
Call run on the concrete type. Going through BUILTIN_COMMANDS works too, but it
gives you every command every linked crate registered — including ones other test
binaries in the same workspace put there.
Wire typed configuration¶
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
struct Settings { host: String, port: u16 }
#[test]
fn command_reads_the_host() {
let app = TestAppBuilder::new(TestWitness::new())
.tool("mytool", "1.0.0")
.config_value(Settings { host: "example.com".into(), port: 8080 })
.build();
let settings = app.typed_config::<Settings>().expect("wired");
assert_eq!(settings.get().host, "example.com");
}
Your config type needs Serialize, Deserialize and JsonSchema here — a
stricter set than App::new asks for, because the schema and the rendered value are
generated eagerly.
Use config(...) instead of config_value(...) when the test needs layered defaults
and overrides rather than a single merged value:
Either one makes app.config_schema() and app.config_value() return Some.
Calling neither leaves both None.
Test the cancellation path¶
The shutdown token is a fresh root token. Derive a child and cancel the parent:
let app = TestAppBuilder::new(TestWitness::new()).tool("t", "1.0.0").build();
let child = app.shutdown.child_token();
app.shutdown.cancel();
assert!(child.is_cancelled());
There is no builder method to supply your own token, so a test that needs the App
to be a child of an outer scope has to cancel app.shutdown directly.
Attach a credentials provider¶
The builder has no method for this, but the field is public:
let mut app = TestAppBuilder::new(TestWitness::new()).tool("t", "1.0.0").build();
app.credentials_provider = Some(std::sync::Arc::new(MyConfig::default()));
assert_eq!(app.credentials().len(), 2);
The same applies to trailing args, via App::with_trailing_args(...) on the built
App.
What build() panics on¶
| Missing | Message |
|---|---|
| metadata | TestAppBuilder: metadata not set |
| version | TestAppBuilder: version not set |
an unparseable version string passed to tool |
parse test version |
All three are panics, not Results. That is deliberate for a test helper.