Soft Assertions¶
Collect multiple assertion failures and report them all at once, instead of stopping at the first one.
This is one of the most valuable features of behave-kit: when a scenario checks several conditions, you see every failure, not just the first.
How it works¶
Soft assertions use contextvars to track an active SoftAssertCollector. Each call to assert_soft() records a failure (if the condition is falsy) without raising. At the end of the scenario, raise_if_failed() raises an AssertionError with a formatted report of every failure.
Activating soft asserts¶
Automatic (via `setup()`):
from behave_kit import setup
def before_all(context):
setup(context, env="staging")
Soft asserts are activated automatically and reset per scenario.
Manual:
from behave_kit import use_soft_asserts
def before_scenario(context, scenario):
use_soft_asserts(context)
Context manager (for unit tests):
from behave_kit import soft_asserts
with soft_asserts() as collector:
assert_soft(1 == 2, "one should equal two")
assert_soft(3 == 3, "three should equal three")
# AssertionError raised here with all failures
API reference¶
- behave_kit.assertions.soft.assert_soft(condition: bool, msg: str = '') None[source]¶
Record a soft assertion failure if
conditionis falsy.
- behave_kit.assertions.soft.assert_soft_equals(actual: object, expected: object, msg: str = '') None[source]¶
Record a soft assertion failure if
actual!=expected.
- behave_kit.assertions.soft.assert_soft_true(condition: object, msg: str = '') None[source]¶
Record a soft assertion failure if
conditionis not truthy.
- behave_kit.assertions.soft.assert_soft_is_none(value: object, msg: str = '') None[source]¶
Record a soft assertion failure if
valueis not None.
- behave_kit.assertions.soft.assert_soft_raises(expected_exception: type[BaseException] | tuple[type[BaseException], ...], func: Callable[[], object], msg: str = '') None[source]¶
Record a soft assertion failure if
funcdoes not raiseexpected_exception.
- behave_kit.assertions.soft.use_soft_asserts(context: Context) SoftAssertCollector[source]¶
Activate soft assertions for the given Behave
context.Attaches a fresh SoftAssertCollector to
context._behave_kit_softand makes it the active collector for assert_soft and friends.
- behave_kit.assertions.soft.soft_asserts() Iterator[SoftAssertCollector][source]¶
Context manager for soft assertions outside of Behave (e.g. unit tests).
Raises an AssertionError summarizing every failure when the block exits without an exception of its own.
- class behave_kit.assertions.soft.SoftAssertCollector[source]¶
Bases:
objectAccumulates assertion failures without raising immediately.
- assert_soft(condition: bool, msg: str = '') None[source]¶
Record a failure if
conditionis falsy.- Parameters:
condition – Boolean (or bool-coercible) value to check.
msg – Optional message describing the failure.
- assert_soft_equals(actual: object, expected: object, msg: str = '') None[source]¶
Record a failure if
actualis not equal toexpected.- Parameters:
actual – The value produced by the code under test.
expected – The value
actualis expected to match.msg – Optional override message for the failure.
- assert_soft_is_none(value: object, msg: str = '') None[source]¶
Record a failure if
valueis notNone.- Parameters:
value – The value to check.
msg – Optional override message for the failure.
- assert_soft_raises(expected_exception: type[BaseException] | tuple[type[BaseException], ...], func: Callable[[], object], msg: str = '') None[source]¶
Record a failure if
funcdoes not raiseexpected_exception.- Parameters:
expected_exception – Exception type (or tuple of types) that
funcis expected to raise.func – Zero-argument callable to invoke.
msg – Optional override message for the failure.
- assert_soft_true(condition: object, msg: str = '') None[source]¶
Record a failure if
conditionis not truthy.- Parameters:
condition – Value evaluated with scalar-bool coercion.
msg – Optional override message for the failure.
- property failures: list[SoftFailure]¶
Copy of the recorded soft assertion failures.
- report() SoftAssertReport[source]¶
Return an immutable report of all recorded failures.
- class behave_kit.assertions.reporter.SoftAssertReport(failures: list[SoftFailure] = <factory>)[source]¶
Bases:
objectAggregated report of every soft assertion failure in a scenario.
- failures: list[SoftFailure]¶
Examples¶
Basic usage¶
from behave_kit import assert_soft
@then("the API response should be valid")
def step(context):
response = context.response
assert_soft(response.status_code == 200, "status code should be 200")
assert_soft("error" not in response.body, "body should not contain 'error'")
assert_soft(response.body["count"] > 0, "count should be positive")
# All failures collected, reported together at teardown
With equality checks¶
from behave_kit import assert_soft_equals
@then("the user profile should match")
def step(context):
assert_soft_equals(context.user["name"], "Alice", "name mismatch")
assert_soft_equals(context.user["email"], "alice@example.com", "email mismatch")
assert_soft_equals(context.user["age"], 30, "age mismatch")
Checking for None¶
from behave_kit import assert_soft_is_none
@then("no error should be present")
def step(context):
assert_soft_is_none(context.error, "error should be None")
assert_soft_is_none(context.warning, "warning should be None")
Checking for expected exceptions¶
from behave_kit import assert_soft_raises
@then("the parser should reject invalid input")
def step(context):
assert_soft_raises(ValueError, lambda: int("not a number"))
assert_soft_raises(KeyError, lambda: context.data["missing"])
# Failures are collected and reported together at teardown
assert_soft_raises also accepts a tuple of exception types:
assert_soft_raises((ValueError, KeyError), lambda: parse_risky_input())
Inspecting the report¶
from behave_kit import use_soft_asserts, assert_soft
@given("I have a soft assert collector active")
def step(context):
collector = use_soft_asserts(context)
context.collector = collector
@then("the soft assert collector should have {count:d} failures")
def step(context, count):
assert len(context.collector.failures) == count
Clearing failures¶
@then("I clear the soft assert failures")
def step(context):
context._behave_kit_soft.clear()
Diff-based assertions¶
In addition to soft asserts, behave-kit provides diff-based comparison
assertions that show exactly which fields differ instead of a bare
AssertionError.
assert_json_equals¶
Deep-compare two objects and report every difference:
from behave_kit import assert_json_equals, CompareOptions
@then("the response body should match the expected JSON")
def step(context):
expected = {"name": "Alice", "age": 30, "roles": ["admin", "user"]}
assert_json_equals(context.response.body, expected)
With options:
from behave_kit import assert_json_equals, CompareOptions
@then("the response should match ignoring timestamps")
def step(context):
opts = CompareOptions(ignore_keys=frozenset({"created_at", "updated_at"}))
assert_json_equals(context.response.body, context.expected, options=opts)
Ignoring order in lists:
opts = CompareOptions(ignore_order=True)
assert_json_equals(actual_list, expected_list, options=opts)
assert_dict_contains¶
Assert that a dict contains every key/value pair from a subset:
from behave_kit import assert_dict_contains
@then("the response should contain the expected fields")
def step(context):
assert_dict_contains(
context.response.body,
{"status": "ok", "count": 5},
)
assert_list_ordered¶
Assert that a list is sorted:
from behave_kit import assert_list_ordered
@then("the results should be sorted by name")
def step(context):
assert_list_ordered(context.results, key=lambda item: item["name"])
assert_table_equals¶
Compare two Behave data tables:
from behave_kit import assert_table_equals
@then("the data table should match")
def step(context):
assert_table_equals(context.table, context.expected_table)
Deep comparison engine¶
- behave_kit.assertions._matchers.deep_compare(actual: object, expected: object, options: CompareOptions | None = None) DiffResult[source]¶
Recursively compare
actualtoexpectedand report all differences.
- class behave_kit.assertions._matchers.CompareOptions(ignore_keys: frozenset[str] = frozenset({}), float_tolerance: float = 1e-09, ignore_order: bool = False, datetime_tolerance: ~datetime.timedelta | None = None, custom_matchers: dict[type, ~collections.abc.Callable[[~typing.Any, ~typing.Any], bool]] = <factory>)[source]¶
Bases:
objectTunable behavior for deep_compare.
- class behave_kit.assertions._matchers.DiffResult(equal: bool, diffs: list[Diff] = <factory>)[source]¶
Bases:
objectOutcome of a deep_compare call.
- class behave_kit.assertions._matchers.Diff(path: str, expected: object, actual: object, message: str)[source]¶
Bases:
objectA single difference found while comparing
actualtoexpected.
CompareOptions supports:
ignore_keys— skip specific keys during comparisonfloat_tolerance— tolerance for floating-point comparisons (default: 1e-9)ignore_order— treat lists as unordered setsdatetime_tolerance— tolerance for datetime comparisonscustom_matchers— per-type custom comparison functions
from behave_kit import CompareOptions, deep_compare
from datetime import timedelta
opts = CompareOptions(
ignore_keys=frozenset({"id", "timestamp"}),
float_tolerance=0.01,
datetime_tolerance=timedelta(seconds=5),
custom_matchers={
MyModel: lambda actual, expected: actual.id == expected.id,
},
)
result = deep_compare(actual_data, expected_data, opts)
if not result.equal:
for diff in result.diffs:
print(f" at {diff.path}: {diff.message}")