Assertions — Expect and AssertField¶
The complete pybuggy assert layer, built on matchcrest. You
never create these objects manually: pybuggy assembles the check configuration when you
call the endpoint, builds Expect lazily on first access to response.expect, and
hands you the field-level assert from response.expect('path').
The response of with endpoint(...) as response: is a ResponseWrapper — a context
manager whose exit does not suppress exceptions. Besides .expect it exposes
.response: the raw resq.http.Response (status code, headers, the undecoded body);
the wrapper does not proxy resq attributes, so reach through .response for anything
the assert layer does not cover.
Common parameters¶
Every check method follows one template — it calls assert_that internally and returns
its own object for chaining. Universal kwargs:
reason: str = ""— the error message prefix.any: bool = False— element-iteration mode; effective only within_array=True:any=False(default) requires all list elements to match,any=True— at least one. Passingany=Truewithoutin_array=TrueraisesValueError.timeout/delay— per-call override of the polling baseline for a single check (see Polling).
.value (an AssertField property) returns the resolved value without a check;
calling the field (field(index=0), field(search=...)) drills one level deeper.
Expect — response-level checks¶
Obtained from response.expect; each method returns Expect for chaining.
| Method | What it checks |
|---|---|
has_status_code(code) |
Response code equals code (int, a requests.codes name like "ok", or an Enum) |
has_header(key, value=None, ...) |
Header key exists; with value — the value matches |
json_has_data_by_key(key) |
Body contains key key with a non-None value |
json_has_not_data_by_key(key) |
Body lacks key key (or its value is None) |
json_contains_key(key) |
Body contains key key (nested when passed a list) |
jsonschema_is_valid(schema) |
Body validates against a json schema (dict or .json path) |
jsonschemas_is_valid(schemas_dir, status_code) |
Body validates against the first <status_code>* file in the directory; silently skips when absent |
has_header(key, value=None, *, contains=None, startswith=None, endswith=None, count=None):
without value — header presence (optional substring/prefix/suffix filter, optional
count); with value — the header value (exact, or contains/startswith/endswith).
Keys and values compare case-insensitively; count combined with value →
ValueError.
Field-level entry¶
field = response.expect("data.items") # dotted path from the body root
field = response.expect("$.data.items[*]") # jsonpath from the body root
field = response.expect() # the whole response body
Expect.__call__(search=None, *, index=None, hook=None, in_array=False):
search— a dotted path (a.b.c) or a jsonpath; always resolved from the root of the response body — spell out the full path including the envelope key when the API wraps its payload.Noneselects the whole response body.index— an optional list index applied after the search.hook— a callable applied to the resolved value (a non-callable →TypeError).in_array— treat the value as a list for per-elementany.
jsonpath rule: $ counts from the response body root.
$.data.items[*] → body["data"]["items"]; $.data[0].name →
body["data"][0]["name"].
AssertField — field-level check catalog¶
The context resolves the path from the root of the response body on every path.
All methods except raise_exc / not_raise_exc accept reason/any/timeout/delay.
Membership and containment¶
| Method | What it checks |
|---|---|
contains(value) |
Value contains value (substring for str, membership for list, key presence for dict) |
not_contains(value) |
Value does not contain value |
contains_dict(dct) |
Dict contains all key/value pairs from dct |
is_in(value) |
Value is an element of value (value is the container) |
is_not_in(value) |
Value is not an element of value |
is_subset(value) |
Iterable value is a subset of value |
is_intersect(value) |
Iterable value shares at least one element with value |
is_disjoint(value) |
Iterable value shares no elements with value |
Argument direction: in
is_in/is_subset/is_intersect/is_disjoint,valueis the second operand (the container/superset).is_subset/is_intersect/is_disjointbuild sets viaset()— both operands must be iterable and hashable.
response.expect("name").contains("abc")
response.expect("tags").is_in(["x", "y"])
response.expect("filters").is_subset({"a": 1, "b": 2})
Equality and emptiness¶
| Method | What it checks |
|---|---|
equal_to(value) |
Equals value; strict=True → identity (is) |
not_equal_to(value) |
Not equal; strict as in equal_to |
empty() / not_empty() |
Empty/falsy — non-empty/truthy |
Number comparison¶
| Method | What it checks |
|---|---|
greater_than(value) |
> value; or_equal=True → >= |
lesser_than(value) |
< value; or_equal=True → <= |
Length¶
| Method | What it checks |
|---|---|
has_length(value) |
len(value) == value |
has_length_greater(value) / has_length_lesser(value) |
Strictly greater/less |
Strings and URLs¶
| Method | What it checks |
|---|---|
startswith(value) / endswith(value) |
Prefix / suffix |
match_regex(pattern) |
re.match semantics — anchored to the start |
is_url() |
Valid URL; is_live=True — reachable (GET → 2xx); allowed_protocols — allowed schemes (default ['https','http']) |
Dates¶
| Method | What it checks |
|---|---|
has_date(value) |
Date/datetime equals value |
has_date_greater(value) / has_date_lesser(value) |
Strictly greater/less |
Dates compare by timestamp: a
dateconverts to midnight, adatetimeto its own timestamp — compare values of the same type.
Exceptions (context managers)¶
| Method | What it checks |
|---|---|
raise_exc(expected_exc) |
Accessing the value raises one of expected_exc |
not_raise_exc() |
Accessing the value raises nothing |
with response.expect("missing").raise_exc(KeyError):
...
with response.expect("ok").not_raise_exc() as value:
assert value == "abc"
Drill-down and arrays¶
- Drill:
field(search=..., index=..., hook=...)returns a newAssertFieldover the extended context (dotted steps →index→hook, in that order). in_array:any=False(default) requires all elements to match;any=True— at least one.- All elements satisfy a set: select the value list with jsonpath
$.data[*].fieldand applyis_subset/is_inover the list. - Element absent among array elements:
$.data[*].field+not_contains(scalars) oris_disjoint(set semantics). - Custom element lookup by predicate: pass a regular lookup function as a
hookover the array (expect("data")for the array under the envelope key, orexpect()for a root-level array); the hook returns the found element (Noneon no match —Nonefails the check). - Empty jsonpath result (including
$[*]over an empty array) raisesAssertionError("No results") — check emptiness viahas_length(0)over the root.
response.expect("data.items", in_array=True).equal_to(2, any=True) # at least one == 2
response.expect("data.items")(index=0).equal_to(1) # drill by index
response.expect("data.name")(hook=str.upper).equal_to("ABC") # hook before comparison
response.expect("data").has_length_greater(0) # the data value is non-empty
response.expect("$.data[0].name").equal_to("abc") # data[0].name
response.expect("$[*].status").is_subset(["active", "idle"]) # every ∈ set
response.expect("$[*].request.test_id").not_contains(test_id_b) # none equals
def _mock_body(items, test_id, path, method):
for item in items:
req = item["request"]
if (req["test_id"], req["path"], req["method"]) == (test_id, path, method):
return _normalize_body(item["response"]["body"])
return None
response.expect()(hook=lambda items: _mock_body(items, tid, "/api/shared", "POST")).equal_to({"owner": "A1"})
Auto-check¶
On the first access to response.expect, pybuggy runs a lazy auto-check once (unless
use_autocheck=False — on the Endpoint or on a single call). The path depends on how the
endpoint was called:
- Positive —
with endpoint(...) as response:→ the expected status (whenexpected_statusis set) → the body is parsed as JSON → validation against the firstschemas/<status>*.json(silently skipped when the directory or file is absent). - Negative —
with endpoint.error(...) as response:→ the body is parsed as JSON only. Status and JSON schema are not checked — verify them explicitly (response.expect.has_status_code(400)).
Envelope keys are not verified by the auto-check; assert them explicitly via
json_has_data_by_key / json_has_not_data_by_key when the API contract requires them.
An explicit has_status_code(200) duplicates only the status part of the positive path —
that is normal.
def test_initiate(post_clients_calls_initiate: Endpoint):
with post_clients_calls_initiate(json=Request(order_id=1)) as response:
response.expect.has_status_code(200) # auto-check already ran on this line
def test_initiate_error(post_clients_calls_initiate: Endpoint):
with post_clients_calls_initiate.error(json={"name": "x"}) as response:
response.expect.has_status_code(400) # negative path: status is your job
response.expect("error.message").not_empty()
Polling¶
timeout/delay from the configuration form the baseline. The check repeats until it
passes or timeout expires; between attempts the response is re-fetched in place by
replaying the same request, pausing delay. Per-call timeout/delay kwargs override
the baseline for a single check; None means one attempt without polling.
Pluggable classes¶
assert_field_class / assert_response_class plug in custom subclasses; they must
inherit the built-ins. Both are configured in the tool config — see
Pluggable assert classes.