Newsletter
TechAnV Blog
Get updates on security engineering, Rust, eBPF, and DevSecOps. No spam, unsubscribe anytime.
Check your inbox and click the confirmation link to complete your subscription.
Using namedtuple for pytest parameterized tests#
I’m writing some quite complex pytest parameterized tests this morning, and I was finding it a little bit hard to read the test cases as the number of parameters grew.
Here’s a pattern I figured out using Python’s namedtuple to make the test cases easier to read and edit:
1from collections import namedtuple2import pytest3
4ManageTableTest = namedtuple(5 "ManageTableTest",6 (7 "description",8 "setup_post_data",9 "post_data",10 "expected_acls",11 "should_fail_then_succeed",12 "expected_audit_rows",13 ),14)15
16@pytest.mark.asyncio17@pytest.mark.parametrize(18 ManageTableTest._fields,19 (20 ManageTableTest(21 description="Group: add insert-row",22 setup_post_data={},23 post_data={"group_permissions_staff_insert-row": "on"},24 expected_acls=[25 {26 "group_name": "staff",27 "actor_id": None,28 "action_name": "insert-row",29 "database_name": "db",30 "resource_name": "t",31 }32 ],33 should_fail_then_succeed=[34 dict(35 actor={"id": "simon", "is_staff": True},36 action="insert-row",37 resource=["db", "t"],38 ),39 ],40 expected_audit_rows=[41 {42 "group_name": "staff",43 "actor_id": None,44 "action_name": "insert-row",45 "database_name": "db",46 "resource_name": "t",47 "operation_by": "root",48 "operation": "added",49 }50 ],51 ),52 ),53)54def test_manage_table_permissions(55 description,56 setup_post_data,57 post_data,58 expected_acls,59 should_fail_then_succeed,60 expected_audit_rows,61):62 # Tests go hereYou can see a more complete example here.
There are a couple of tricks here. I’m defining this namedtuple with the fields that I know I want to use in the tests:
1ManageTableTest = namedtuple(2 "ManageTableTest",3 (4 "description",5 "setup_post_data",6 "post_data",7 "expected_acls",8 "should_fail_then_succeed",9 "expected_audit_rows",10 ),11)I’m doing this purely to be able to use keyword arguments when defining my tests, which are much easier to read than a ordered list of arguments.
The pytest.mark.parametrize decorator is usually called with a comma-separated string of field names, which would look like this:
1@pytest.mark.parametrize(2 "description,setup_post_data,post_data,expected_acls,should_fail_then_succeed,expected_audit_rows", [3 ManageTableTest(description="...",)4])5def test_manage_table_permissions(...)That’s a bit ugly, and you end up duplicating the list of fields. Instead, I reuse the definition from the namedtuple like this:
1@pytest.mark.parametrize(2 ManageTableTest._fields,3 (4 ManageTableTest(5 description="...",6 )7 )One last trick: that description= field isn’t actually used by my code. Initially I had a code comment instead.
Then I found myself running pytest -x --pdb to drop into a debugger in a failing test but getting confused over which of the parameterized tests I was in. So I added the description argument so I can type description in the debugger and find out which test case failed.