Fluent API¶
Chaining¶
Assertions chain into a single statement that reads like a sentence:
assert_that("foo").is_length(3).starts_with("f").ends_with("oo")
assert_that([1, 2, 3]).is_type_of(list).contains(1, 2).does_not_contain(4, 5)
assert_that(fred).has_first_name("Fred").has_last_name("Smith").has_shoe_size(12)
assert_that(people).is_length(2).extracting("first_name").contains("Fred", "Joe")
Universal negation¶
The .not_ property inverts the next assertion in the chain, so there is no need for dedicated
is_not_* methods:
assert_that(5).not_.is_none()
assert_that("abc123").not_.is_alpha()
assert_that([3, 1, 2]).not_.is_sorted()
assert_that(42).not_.is_in(1, 2, 3)
assert_that("hello").not_.is_instance_of(int)
Chaining continues normally after a negated assertion:
assert_that(5).not_.is_none().is_positive()
assert_that("hello").not_.is_empty().is_length(5).is_alpha()
Static types survive the inversion too. A negated step hands back the same assertion type the un-negated one would, so the value type is kept:
And a wrong-domain assertion after it is still caught:
# type error: is_greater_than is not a string assertion
assert_that("x").not_.is_empty().is_greater_than(0)
.not_ works with descriptions, soft assertions, warn mode, and matchers:
assert_that(5).described_as("my check").not_.is_positive()
# assertpy2.AssertionFailure: [my check] Expected <5> to NOT satisfy: is_positive()
with soft_assertions():
assert_that(5).not_.is_positive() # collected, not raised
assert_warn("hello").not_.is_alpha() # logs a warning
assert_that(-5).not_.satisfies(match.is_positive())
assert_that([1, -2, 3]).not_.each(match.is_positive())
Note
Only assertions can be negated. Steps that configure or transform - not assert - raise a
TypeError under .not_:
described_as(),decoded_as(),at_json_path()- the collection pipeline (
extracting(),filtered_on(),mapped(),first(), ...) eventually()/eventually_sync().
Place them before .not_ (or negate the assertion that follows them) instead:
Collection pipeline¶
Pipeline methods transform the value before asserting. Each returns a new builder, so the original value is unchanged and steps chain freely.
filtered_on(predicate)¶
Filter elements by a callable or matcher:
assert_that([1, -2, 3, -4]).filtered_on(lambda x: x > 0).is_length(2)
assert_that(items).filtered_on(match.is_positive()).is_not_empty()
assert_that(users).filtered_on(match.has_property("active")).is_length(5)
mapped(func)¶
Transform each element:
assert_that(["a", "b", "c"]).mapped(str.upper).contains("A", "B")
assert_that(users).mapped(lambda u: u.name).contains("Alice", "Bob")
flat_mapped(func)¶
Transform and flatten:
assert_that(["ab", "cd"]).flat_mapped(list).contains("a", "b", "c", "d")
assert_that(users).flat_mapped(lambda u: u.tags).contains("admin", "user")
first() / last() / element(index) / single()¶
Navigate to a specific element and assert on it:
assert_that([10, 20, 30]).first().is_equal_to(10)
assert_that([10, 20, 30]).last().is_equal_to(30)
assert_that([10, 20, 30]).element(1).is_equal_to(20)
assert_that([42]).single().is_equal_to(42)
Warning
first(), last(), and single() raise ValueError on an empty collection (single() also on
more than one element). element(index) raises IndexError when the index is out of range.
Chaining pipeline steps¶
Pipeline methods return a new builder, so they chain with each other and with any assertion:
assert_that(orders).filtered_on(lambda o: o.status == "FAILED").mapped(
lambda o: o.total
).first().satisfies(match.is_positive())
assert_that(items).filtered_on(match.is_positive()).not_.is_empty()
What the pipeline does to the static type
The two halves behave differently, and the difference is the useful part.
first(), last(), element() and single() narrow the chain to the element, so a list of
orders hands you an order and .value is typed as one. Every assertion is available on it,
type-specific ones included.
mapped() and flat_mapped() stay a collection, re-typed to whatever the function returns.
assert_that([10, 20, 30]).first().is_positive() # element: numeric assertions apply
assert_that(["a", "bb"]).mapped(len).contains(1, 2) # still a collection, now of int
See typed narrowing for the whole picture.