Skip to main content

Assertions

Struct Assertions 

Source
pub struct Assertions {
    pub status: Option<u16>,
    pub status_in: Option<Vec<u16>>,
    pub headers: BTreeMap<String, Option<String>>,
    pub body_contains: Option<String>,
    pub body_matches: Option<String>,
    pub elapsed_ms_under: Option<u64>,
    pub json: BTreeMap<String, Value>,
    pub not: Option<NotAssertions>,
}
Expand description

The assertions block of a request, exactly as it appears on disk.

Each field is a separate kind of check, and each entry within a field is one assertion — headers with three entries is three assertions, reported individually. All of them are evaluated on every response; none short-circuit the others, because “which of my expectations held” is the question this feature exists to answer and stopping at the first failure would answer it only partially.

Unknown keys are rejected, like everywhere else in Sendra’s schema: an assertion silently ignored because of a typo is worse than no assertion at all, since it reads as a check that is passing.

Fields§

§status: Option<u16>

The exact status code the response must carry.

Equality against one code rather than a class (2xx) or a range: the two are not the same assertion, and “this endpoint answers 201” is the one worth writing down. A class matcher can be added as its own key later without changing what this one means.

§status_in: Option<Vec<u16>>

The status code must be one of these.

Its own key rather than folding into status (a list there would change what a bare status: 200 means) — status is “exactly this code”, status_in is “one of these codes”, and a file should be able to write either without the other’s presence changing its meaning.

§headers: BTreeMap<String, Option<String>>

Headers the response must carry, by name.

A value asserts the header is present and equal to it; a null value (x-request-id: with nothing after it) asserts only that the header is there. One key covers both because they are the same assertion with and without an expectation about the value, and a second key (headers_present) would make the file say twice what the value’s presence already says.

Names are matched case-insensitively, because HTTP header names are. Values are matched exactly: content-type: application/json does not match application/json; charset=utf-8. That is the strict reading, and the honest one — a substring match would quietly accept application/json-seq too. When a server decorates a value, assert the whole value or drop to presence-only.

A repeated header (set-cookie) passes if any of its values matches.

§body_contains: Option<String>

A substring the response body must contain, matched case-sensitively on the body as printed.

§body_matches: Option<String>

A regular expression the response body must match, anywhere in it — the same “somewhere in the body” reach as body_contains, not an anchored whole-body match, so body_matches: '"id":\s*\d+' finds that pattern wherever it sits.

The engine is regex, already in the dependency tree as a transitive dependency of jsonpath-rust — this adds no new crate, only a direct declaration of one already being built.

The pattern is checked when the assertion runs, not when the file is loaded, for the same reason a JSON path is: a stricter release of regex should not start rejecting files that used to load, for a request Sendra could still send. An invalid pattern is a failed assertion naming the parse error, not a panic or a load-time error.

§elapsed_ms_under: Option<u64>

The response must have arrived in under this many milliseconds.

Backed by Response::elapsed, which is wall-clock time for the request as sent — DNS, connect and TLS included, the same number a person timing the request by hand would get. A strict “under”, not “at or under”: a threshold is normally chosen as a round number the response should beat, and elapsed_ms_under: 500 reads as “faster than half a second,” which an exact 500ms response is not.

§json: BTreeMap<String, Value>

JSON path expressions mapped to the value each must select.

json:
  $.user.id: 42
  $.user.name: ada
  $.tags: [a, b]

The expected value is written as YAML and held as a serde_json::Value — parsed once, when the file is loaded, into the form it will be compared against, so a value that has no JSON equivalent is a parse error naming the file rather than a surprise at response time.

A path must select exactly one value. Nothing matched, or several matched, is a failure with that stated: $.users[*].id against three users is a question with no single answer, and picking the first would make the assertion depend on ordering the author never specified.

The engine is jsonpath-rust, chosen over serde_json_path, the other RFC 9535 implementation, on maintenance and stability: at the time of writing jsonpath-rust is at 1.0 with releases landing this year, while serde_json_path has not released since February 2025 and is still pre-1.0. Both are correct and both query serde_json::Value directly, which is what keeps this dependency swappable if that ever changes: it is confined to Assertions::evaluate, behind a path string and a value comparison. Beyond a bare equality value, a path may map to an operator object with exactly one of these keys:

json:
  $.count: { greater_than: 5 }             # numeric: actual > 5
  $.count: { greater_than_or_equal: 5 }    # numeric: actual >= 5
  $.count: { less_than: 5 }                # numeric: actual < 5
  $.count: { less_than_or_equal: 5 }       # numeric: actual <= 5
  $.tags: { contains: b }   # substring of a string, or array membership
  $.tags: { length: 2 }                     # array/string length equals 2
  $.tags: { length: { greater_than: 1 } }   # length compared, not just equal
  $.id: { matches: '^[0-9a-f-]{36}$' }      # regex, scoped to this path's
                                             # string value — see `body_matches`
                                             # for the whole-body equivalent

See the module docs for exactly how a bare value is told apart from an operator, and what that costs.

§not: Option<NotAssertions>

Every assertion in this block, inverted: passes exactly when the wrapped one would have failed, and vice versa. See the module docs for what a wrapper buys over a not_-prefixed key per assertion type, and for why a hard error underneath — a malformed path, an invalid regex, a type mismatch — is not something this can turn into a pass.

Implementations§

Source§

impl Assertions

Source

pub fn is_empty(&self) -> bool

True when the block asserts nothing — assertions: {}, or a block whose every key was omitted.

Source

pub fn evaluate(&self, response: &Response) -> AssertionReport

Check every assertion against response and report all of them.

The response must be the one that actually came back from the request as sent — after variable substitution and after config was applied — since that is the request the assertions were written about.

Trait Implementations§

Source§

impl Clone for Assertions

Source§

fn clone(&self) -> Assertions

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Assertions

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Assertions

Source§

fn default() -> Assertions

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for Assertions

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl PartialEq for Assertions

Source§

fn eq(&self, other: &Assertions) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

impl Serialize for Assertions

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more
Source§

impl StructuralPartialEq for Assertions

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more