Everything so far was tools. Here are the three jobs they were picked up for. Each one shows up weekly.
Count what the screen does not show
The interface lists orders; the question is different: "what is the total of PAID orders for the day?" That number is not on the screen — but it is in the data.
paid = [o for o in orders if o["status"] == "PAID"]
total = sum(o["total"] for o in paid)
print(f"Paid: {len(paid)}, total {total}")
sum() adds numbers from a sequence. Next to it live min(), max() and
len() — those four cover most summaries.
Counting by group is convenient with a dict accumulator:
by_status = {}
for order in orders:
status = order["status"]
by_status[status] = by_status.get(status, 0) + 1
get(status, 0) is what holds the whole construction together: a new status has
no counter yet, and instead of an error you get zero.
Compare the total with the parts
The classic check: the sum of items must match the order total. Defects like this are invisible in the interface — only arithmetic finds them.
def total_adds_up(order):
computed = sum(i["qty"] * i["price"] for i in order["items"])
return computed == order["total"]
broken = [o["id"] for o in orders if not total_adds_up(o)]
assert not broken, f"total does not add up for: {broken}"
Note the last line: the identifiers of the bad orders go into the message. "Does not add up for three" is useless; "does not add up for ord-042, ord-107" lets you open them.
Careful with fractions: 0.1 + 0.2 is not exactly 0.3 in any language. Money is
therefore either counted in cents as integers or compared with a tolerance:
assert abs(computed - order["total"]) < 0.01
Find what is missing and what is extra
Two exports — from the interface and from the database, from the old version and the new one. The question is always the same: what is missing and what appeared. With lists that is awkward; with sets it is one line.
A set is a collection without order and without duplicates:
live example
before = {"ord-01", "ord-02", "ord-03"}
after = {"ord-02", "ord-03", "ord-04"}
print(before - after) # {'ord-01'} — missing
print(after - before) # {'ord-04'} — extra
print(before & after) # in both
Run
Running examples is part of paid access. There the same code runs inside the article: editor, run and check next to the paragraph. Free week →
From a list of dicts a set is built by the key field:
from_db = {o["id"] for o in db_export}
from_api = {o["id"] for o in response["items"]}
missing = from_db - from_api
extra = from_api - from_db
assert not missing, f"missing from the API: {sorted(missing)}"
assert not extra, f"extra in the API: {sorted(extra)}"
sorted() in the message is not decoration: a set has no order, and without it
the same discrepancy prints differently every time.
Compare content, not just membership
Matching identifiers is not enough: an order can be present but with a different total. Then you compare fields:
db = {o["id"]: o for o in db_export}
for order in response["items"]:
expected = db[order["id"]]
assert order["total"] == expected["total"], (
f"{order['id']}: API says {order['total']}, database says {expected['total']}")
The first line is a trick worth remembering: a list turns into a dict keyed by id, and any element is then found instantly, without scanning.
What to solve
In short
sum(),len(),min(),max()cover most summaries; group counting is a dict accumulator withget(key, 0).- Comparing the total with its parts catches defects invisible in the interface; put identifiers in the message, not counts.
- Money in floats is compared with a tolerance, or counted in cents.
- The difference between two sets is
before - afterandafter - before. - Turn a list into a dict by key when you need fast lookups.
What to read next
- pytest: your first real test — how this is packaged in a project.
- SQL for testers — the other half of the same work: the data comes from a database.