pub struct TestClient { /* private fields */ }Expand description
Fluent HTTP client for integration tests.
Analogous to Spring Boot’s MockMvc or Django’s Client.
Fires requests through the full Axum middleware pipeline using
tower::ServiceExt::oneshot() – no TCP listener required.
Created by TestApp::build().
§Examples
let client = TestApp::new().routes(routes![handler]).build();
// GET request
client.get("/path").send().await.assert_ok();
// POST with JSON body
client.post("/items")
.json(&serde_json::json!({"name": "foo"}))
.send().await
.assert_status(201);
// PUT with header
client.put("/items/1")
.header("authorization", "Bearer token")
.json(&serde_json::json!({"name": "bar"}))
.send().await
.assert_ok();Implementations§
Source§impl TestClient
impl TestClient
Sourcepub const fn state(&self) -> &AppState
pub const fn state(&self) -> &AppState
Returns a reference to the AppState wired into this test app’s router.
Sourcepub fn published_events<E: Event>(&self) -> Vec<E>
pub fn published_events<E: Event>(&self) -> Vec<E>
Every recorded publication of event type E, deserialized.
Events are recorded synchronously at publish time, so this works whether or not the listeners (sync or durable) have run.
Sourcepub fn assert_event_published<E: Event>(&self)
pub fn assert_event_published<E: Event>(&self)
Assert that at least one event of type E was published during the test.
§Panics
Panics if no event of type E was recorded.
Sourcepub fn advance_clock(&self, duration: Duration)
pub fn advance_clock(&self, duration: Duration)
Step the test clock forward by duration.
Only effective when the app was configured with a
crate::time::TickingClock via TestApp::with_clock. Calling this
with a crate::time::FixedClock or without any custom clock is a
safe no-op — time stays where it is.
This method only affects the wall-clock time reported by the
crate::time::Clock extractor. Tokio’s runtime timer (used by
tokio::time::sleep, tokio::time::Instant, etc.) is not affected.
use autumn_web::test::TestApp;
use autumn_web::time::TickingClock;
use chrono::{TimeZone, Utc};
use std::time::Duration;
let clock = TickingClock::starting_at(Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap());
let client = TestApp::new().with_clock(clock).build();
client.advance_clock(Duration::from_secs(86400)); // advance 1 daySourcepub fn into_router(self) -> Router
pub fn into_router(self) -> Router
Unwrap the underlying axum::Router out of the TestClient.
Sourcepub const fn probes(&self) -> &ProbeState
pub const fn probes(&self) -> &ProbeState
Return the crate::probe::ProbeState wired into this test app’s router.
Use this to drive readiness/liveness transitions in integration tests and verify the HTTP probe endpoints reflect state changes.
Sourcepub fn sent_mail(&self) -> Vec<SentMail>
pub fn sent_mail(&self) -> Vec<SentMail>
Returns all emails sent during this test, in the order they were sent.
The built-in recorder is installed automatically — no
.with_mail_interceptor(…) call is required.
§Example
client.post("/signup").json(&body).send().await.assert_ok();
let mail = &client.sent_mail()[0];
assert_eq!(mail.subject, "Welcome!");Sourcepub fn assert_email_count(&self, n: usize) -> &Self
pub fn assert_email_count(&self, n: usize) -> &Self
Asserts that exactly n emails were sent, panicking with a list of
what was actually sent on failure.
Returns &self for chaining.
§Panics
Panics when the count does not match.
Sourcepub fn assert_no_email_sent(&self) -> &Self
pub fn assert_no_email_sent(&self) -> &Self
Asserts that no emails were sent.
Returns &self for chaining.
§Panics
Panics when any emails were sent.
Sourcepub fn assert_email_sent(&self, predicate: impl Fn(&SentMail) -> bool) -> &Self
pub fn assert_email_sent(&self, predicate: impl Fn(&SentMail) -> bool) -> &Self
Asserts that at least one sent email satisfies predicate, panicking
with a list of what was actually sent on failure.
Returns &self for chaining.
§Panics
Panics when no sent email matches.
§Example
client
.assert_email_sent(|m| m.to.iter().any(|a| a == "alice@example.com"))
.assert_email_sent(|m| m.subject == "Welcome!");Sourcepub fn broadcasts(&self) -> Vec<RecordedBroadcast>
pub fn broadcasts(&self) -> Vec<RecordedBroadcast>
Every recorded channel publication, in publish order.
Requires opting in with TestApp::record_broadcasts. Captures both
raw publish text and publish_html HTML/OOB payloads.
§Panics
Panics if TestApp::record_broadcasts was not called.
Sourcepub fn broadcasts_on(&self, topic: &str) -> Vec<RecordedBroadcast>
pub fn broadcasts_on(&self, topic: &str) -> Vec<RecordedBroadcast>
Recorded publications on topic, in publish order.
§Panics
Panics if TestApp::record_broadcasts was not called.
Sourcepub fn assert_broadcast(
&self,
topic: &str,
predicate: impl Fn(&RecordedBroadcast) -> bool,
) -> &Self
pub fn assert_broadcast( &self, topic: &str, predicate: impl Fn(&RecordedBroadcast) -> bool, ) -> &Self
Asserts that at least one publication on topic satisfies predicate.
Returns &Self for chaining.
§Panics
Panics when no matching publication is found, dumping what was
published to topic and nearby topics.
Sourcepub fn assert_broadcast_count(&self, topic: &str, n: usize) -> &Self
pub fn assert_broadcast_count(&self, topic: &str, n: usize) -> &Self
Asserts that exactly n publications were made to topic.
Returns &Self for chaining.
§Panics
Panics when the count does not match, dumping what was published to
topic and nearby topics.
Sourcepub fn assert_no_broadcasts(&self, topic: &str) -> &Self
pub fn assert_no_broadcasts(&self, topic: &str) -> &Self
Asserts that nothing was published to topic.
Returns &Self for chaining.
§Panics
Panics when any publication was made to topic, dumping what was
published to topic and nearby topics.
Sourcepub fn enqueued_jobs(&self) -> Vec<RecordedJob>
pub fn enqueued_jobs(&self) -> Vec<RecordedJob>
Every background-job enqueue captured by the built-in recorder, in the
order they were enqueued (across enqueue, enqueue_after_commit, and
enqueue_in_tx).
The recorder is always on for TestApp::build clients — no opt-in.
§Panics
Panics if called on a TestClient built via TestApp::from_router,
which bypasses recorder wiring.
Sourcepub fn assert_job_enqueued(&self, name: &str) -> &Self
pub fn assert_job_enqueued(&self, name: &str) -> &Self
Assert at least one job with the given registered name was enqueued.
§Panics
Panics, listing every job that was enqueued, if no enqueue with that name was captured.
Sourcepub fn assert_job_enqueued_with(&self, name: &str, payload: Value) -> &Self
pub fn assert_job_enqueued_with(&self, name: &str, payload: Value) -> &Self
Assert at least one job was enqueued with both the given registered
name and an exactly-equal JSON payload.
§Panics
Panics, listing every job that was enqueued, if no enqueue matched both the name and payload.
Sourcepub fn assert_no_jobs_enqueued(&self) -> &Self
pub fn assert_no_jobs_enqueued(&self) -> &Self
Assert no jobs were enqueued at all.
§Panics
Panics, listing every captured enqueue, if any job was enqueued.
Sourcepub async fn perform_enqueued_jobs(&self) -> PerformedJobs
pub async fn perform_enqueued_jobs(&self) -> PerformedJobs
Drain every captured job and dispatch it through its registered handler, awaiting each in enqueue order, so a test can assert the resulting side effects synchronously.
Each captured payload is handed to the same handler the runtime would invoke, so the real deserialization path runs: a payload that cannot be deserialized into the job’s args surfaces as a per-job failure (not a silent miss). The queue is emptied — a second call performs nothing until more jobs are enqueued.
Returns a PerformedJobs report carrying each job’s (name, result);
per-job handler errors (and captured jobs with no registered handler)
are surfaced there rather than swallowed. See
PerformedJobs::assert_all_succeeded.
§Note
TestApp::build starts the in-process job worker by default, and that
worker also drains and runs the same enqueued jobs. Calling this method
therefore executes a job’s side effect an additional time, on top of
the worker’s own run. It is primarily for asserting that a job runs to
completion — surfacing handler/deserialization errors synchronously — not
for counting side effects. Any assertion on a side effect’s count must
account for the worker’s run as well (as the job-recorder integration
tests do: they settle the worker’s run first, then attribute the next
increment to this call).
The helper invokes each job’s registered handler directly and does not
run it through a user-installed
JobInterceptor::intercept_execute,
so
execution-interceptor effects (context injection, metrics, error
injection) are exercised by the in-process worker path, not by this
helper.
§Panics
Panics if called on a TestClient built via TestApp::from_router.
Sourcepub fn get(&self, uri: &str) -> RequestBuilder
pub fn get(&self, uri: &str) -> RequestBuilder
Start building a GET request.
Sourcepub fn post(&self, uri: &str) -> RequestBuilder
pub fn post(&self, uri: &str) -> RequestBuilder
Start building a POST request.
Sourcepub fn put(&self, uri: &str) -> RequestBuilder
pub fn put(&self, uri: &str) -> RequestBuilder
Start building a PUT request.
Sourcepub fn delete(&self, uri: &str) -> RequestBuilder
pub fn delete(&self, uri: &str) -> RequestBuilder
Start building a DELETE request.
Sourcepub fn patch(&self, uri: &str) -> RequestBuilder
pub fn patch(&self, uri: &str) -> RequestBuilder
Start building a PATCH request.
Sourcepub fn options(&self, uri: &str) -> RequestBuilder
pub fn options(&self, uri: &str) -> RequestBuilder
Start building an OPTIONS request (e.g. a CORS preflight).
Sourcepub async fn acting_as(&self, user_id: impl Display) -> &Self
pub async fn acting_as(&self, user_id: impl Display) -> &Self
Establish an authenticated session for user_id without calling the
login endpoint, then return &Self for chaining.
Mints a fresh session containing the app’s configured
auth.session_key (default "user_id") set to user_id, saves it to
the session store the router reads, and seeds the cookie jar with the
session cookie. A subsequent request to a #[secured] / Auth-gated
route then extracts the same identity a real login would produce.
This sets identity only — authorization still runs. A user acted-as here who lacks a required role or scope is still denied.
Analogous to Laravel’s actingAs, Rails’ sign_in, Django’s
force_login, and Phoenix’s log_in_user.
§Panics
Panics if the client has no handle to a session store — i.e. it was
built via TestApp::from_router, or configured with a non-memory
session backend. Use TestApp::build with the default (memory)
session backend for acting_as support.
§Examples
let client = TestApp::new().routes(routes![dashboard]).build();
client.acting_as(42).await;
client.get("/dashboard").send().await.assert_ok();Sourcepub fn log_out(&self) -> &Self
pub fn log_out(&self) -> &Self
Clear the session cookie from the jar, reverting the client to an
unauthenticated state, then return &Self for chaining.
After log_out, a request to a secured route returns its
unauthenticated status (401 / redirect) again. The corresponding
server-side session (if any) is left to expire naturally.
Analogous to Laravel’s Auth::logout, Rails’ sign_out, and Django’s
logout.
Auto Trait Implementations§
impl !RefUnwindSafe for TestClient
impl !UnwindSafe for TestClient
impl Freeze for TestClient
impl Send for TestClient
impl Sync for TestClient
impl Unpin for TestClient
impl UnsafeUnpin for TestClient
Blanket Implementations§
Source§impl<T> AggregateExpressionMethods for T
impl<T> AggregateExpressionMethods for T
Source§fn aggregate_distinct(self) -> Self::Outputwhere
Self: DistinctDsl,
fn aggregate_distinct(self) -> Self::Outputwhere
Self: DistinctDsl,
DISTINCT modifier for aggregate functions Read moreSource§fn aggregate_all(self) -> Self::Outputwhere
Self: AllDsl,
fn aggregate_all(self) -> Self::Outputwhere
Self: AllDsl,
ALL modifier for aggregate functions Read moreSource§fn aggregate_filter<P>(self, f: P) -> Self::Output
fn aggregate_filter<P>(self, f: P) -> Self::Output
Source§fn aggregate_order<O>(self, o: O) -> Self::Outputwhere
Self: OrderAggregateDsl<O>,
fn aggregate_order<O>(self, o: O) -> Self::Outputwhere
Self: OrderAggregateDsl<O>,
Source§impl<T> AutumnDependents for Twhere
T: ?Sized,
impl<T> AutumnDependents for Twhere
T: ?Sized,
Source§fn dependents() -> &'static [RuntimeDependentSpec]
fn dependents() -> &'static [RuntimeDependentSpec]
#[model] overrides via an inherent shadow when dependents exist.Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be
downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further
downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSend for T
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoSql for T
impl<T> IntoSql for T
Source§fn into_sql<T>(self) -> Self::Expression
fn into_sql<T>(self) -> Self::Expression
self to an expression for Diesel’s query builder. Read moreSource§fn as_sql<'a, T>(&'a self) -> <&'a Self as AsExpression<T>>::Expression
fn as_sql<'a, T>(&'a self) -> <&'a Self as AsExpression<T>>::Expression
&self to an expression for Diesel’s query builder. Read moreSource§impl<T> Pointable for T
impl<T> Pointable for T
Source§impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> PolicyExt for Twhere
T: ?Sized,
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<T, Conn> RunQueryDsl<Conn> for T
impl<T, Conn> RunQueryDsl<Conn> for T
Source§fn execute<'conn, 'query>(
self,
conn: &'conn mut Conn,
) -> <Conn as AsyncConnectionCore>::ExecuteFuture<'conn, 'query>
fn execute<'conn, 'query>( self, conn: &'conn mut Conn, ) -> <Conn as AsyncConnectionCore>::ExecuteFuture<'conn, 'query>
Source§fn load<'query, 'conn, U>(
self,
conn: &'conn mut Conn,
) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
fn load<'query, 'conn, U>( self, conn: &'conn mut Conn, ) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
Source§fn load_stream<'conn, 'query, U>(
self,
conn: &'conn mut Conn,
) -> Self::LoadFuture<'conn>where
Conn: AsyncConnectionCore,
U: 'conn,
Self: LoadQuery<'query, Conn, U> + 'query,
fn load_stream<'conn, 'query, U>(
self,
conn: &'conn mut Conn,
) -> Self::LoadFuture<'conn>where
Conn: AsyncConnectionCore,
U: 'conn,
Self: LoadQuery<'query, Conn, U> + 'query,
Stream] with the returned rows. Read moreSource§fn get_result<'query, 'conn, U>(
self,
conn: &'conn mut Conn,
) -> AndThen<Self::LoadFuture<'conn>, LoadNext<Pin<Box<Self::Stream<'conn>>>>>
fn get_result<'query, 'conn, U>( self, conn: &'conn mut Conn, ) -> AndThen<Self::LoadFuture<'conn>, LoadNext<Pin<Box<Self::Stream<'conn>>>>>
Source§fn get_results<'query, 'conn, U>(
self,
conn: &'conn mut Conn,
) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
fn get_results<'query, 'conn, U>( self, conn: &'conn mut Conn, ) -> AndThen<Self::LoadFuture<'conn>, TryCollect<Self::Stream<'conn>, Vec<U>>>
Vec with the affected rows. Read moreSource§impl<T> Scoped for T
impl<T> Scoped for T
Source§fn scope(ctx: &PolicyContext) -> ScopeQuery<'_, Self>
fn scope(ctx: &PolicyContext) -> ScopeQuery<'_, Self>
ScopeQuery for this type. Resolves the
registered scope at .load() time, not here.