Skip to content

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').

from goga_tool_pybuggy.api.asserts import AssertField   # for a type hint

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 with in_array=True: any=False (default) requires all list elements to match, any=True — at least one. Passing any=True without in_array=True raises ValueError.
  • 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 valueValueError.

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. None selects 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-element any.

jsonpath rule: $ counts from the response body root. $.data.items[*]body["data"]["items"]; $.data[0].namebody["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, value is the second operand (the container/superset). is_subset/is_intersect/is_disjoint build sets via set() — 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 date converts to midnight, a datetime to 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 new AssertField over the extended context (dotted steps → indexhook, 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[*].field and apply is_subset / is_in over the list.
  • Element absent among array elements: $.data[*].field + not_contains (scalars) or is_disjoint (set semantics).
  • Custom element lookup by predicate: pass a regular lookup function as a hook over the array (expect("data") for the array under the envelope key, or expect() for a root-level array); the hook returns the found element (None on no match — None fails the check).
  • Empty jsonpath result (including $[*] over an empty array) raises AssertionError ("No results") — check emptiness via has_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:

  • Positivewith endpoint(...) as response: → the expected status (when expected_status is set) → the body is parsed as JSON → validation against the first schemas/<status>*.json (silently skipped when the directory or file is absent).
  • Negativewith 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.