Precisely: Better assertions for Python tests
github.com
github.com
assert foo["bar"] == 123
assert foo["baz"]["bam"]["boo"] = "abc"
I don't see how is that made easier (and less ambiguous) in hamcrest-style assertions...There's no particurly terse why to write an example I'm afraid.
Better error messages was the motivation: PyHamcrest tends to put everything onto one line, which makes it hard to discern structure. In contrast, precisely tries to produce an error message that uses line breaks and indentation to match the structure of the matcher, hopefully making it clear exactly which part of the matcher has failed.
assert all([el in result for el in ['A', 'B']])
There is definitely room for more brainfarts in the above, but introducing a new library with its own semantics into a team also have its costs.That said, I largely agree with you - checking for this exact example from the README file works much better (and IMO more readable) as:
assert set(result) == {"a", "b"}
The precisely example doesn't even check if the result is a list, so it can be anything. result = ["a", "b", "b"]
assert set(result) == {"a", "b"}
In fact that is the first main motivating failure case discussed in the readme. But actually that's simple enough to deal with too: from collections import Counter
assert Counter(result) = {"a": 1, "b": 1}
But would be trickier for mutable / unhashable types (not sure if "precisely" can deal with those).More specifically, writing:
contains_exactly("a", "b")
is equivalent to: contains_exactly(equal_to("a"), equal_to("b")) assert sorted(result) == sorted(['a','b'])(Frozensets are hashable but not sortable but that's probably more niche.)
assert sorted(result) == ['a', 'b']If the author of the library is reading, I think it would be a great addition to have a section discussing the benefit vs writing your own one line checkers.
To take your example from up top: If I'm coming to the code later, and I notice that it's not asserting for the absence of anything, it may be very difficult for me to tell whether that behavior is your intent, or just a brain fart.
It's just another layer of confusion in the many layers of confusion that lead to test rot.
If you were to ask me "Should I use precisely in my tests?", my answer would be: it depends. The main benefit is to better describe the intent of your test, so that the assertion is neither under- nor over-specified, with the intent directly stated.
Assuming your assertion is meant to be an alternative to the example in the README:
assert_that(result, contains_exactly("a", "b"))
I'd suggest that the above states the intention of the assertion, rather than how you check it. For instance, your assertion would allow duplicate elements, whereas the assertion as originally written would suggest that this isn't desired. As other comments have pointed out, you can do things with sorted, Counter or set (depending on exactly what you want to assert), but why worry about what trick to use when you could just directly state your intention?The assertion using precisely is also (arguably) easier for a reader to know what is (and isn't) being asserted in the test, and makes the test less brittle since you're not accidentally asserting more than intended (for instance, it's common to assert equality with a list, even though you don't actually care about order).
Another common case is when you want to make assertions on a collection, but equality would check too much. For instance, suppose you have a function that fetches users from a database. The fetch can return the users in any order, and you just want to check the names of the returning users, so you can write something like:
assert_that(result, contains_exactly(
has_attr(name="Alice"),
has_attr(name="Bob"),
))
How would you write something that means the same thing without precisely? The order isn't deterministic, so you can't write something like: assert result[0].name == "Alice"
assert result[1].name == "Bob
assert len(result) == 2
We could sort the users by name before making the assertion: result = sorted(result, key=lambda user: user.name)
assert result[0].name == "Alice"
assert result[1].name == "Bob
assert len(result) == 2
Personally, I prefer the precisely assertion!What about something like an equality assertion?
assert set(result) == {
User(id=1, name="Alice", email_address="alice@example.com"),
User(id=2, name="Bob, email_address="bob@example.com"),
}
Now we've over-specified our test -- we need to know irrelevant details like the ID and e-mail address of the users, which might change and break this test even when the functionality we care about still works. We'll also break the test if we add any more attributes to users.As you've mentioned, there's a cost to learning precisely. Even if you're familiar with the library, then you're still potentially writing a more complex assertion (where more can go wrong) than just (for instance) an equality assertion. In my experience, on many projects, the ability to state the intent of assertions precisely has far outweighed the downsides, but that's from a position of already being comfortable with the library.
I tend to try and lean on the language as much as possible before pulling in libraries like this. This is the sort of thing that you end up regretting a year or two later when the author abandons the project and you can no longer get help for bugs. Unlike vanilla language assertions, you won't need to rewrite your entire test suite when that situation inevitably comes up.
pytest will give you really awesome feedback with vanilla/basic 'assert x in y' statements