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.
Quick and dirty mock testing with mock_calls#
I needed to write a test that checked for a really complex sequence of mock calls for s3-credentials#3.
I ended up using the following trick, using pytest-mock:
1def test_create(mocker):2 boto3 = mocker.patch("boto3.client")3 runner = CliRunner()4 with runner.isolated_filesystem():5 result = runner.invoke(cli, ["create", "pytest-bucket-simonw-1", "-c"])6 assert [str(c) for c in boto3.mock_calls] == [7 "call('s3')",8 "call('iam')",9 "call().head_bucket(Bucket='pytest-bucket-simonw-1')",10 "call().get_user(UserName='s3.read-write.pytest-bucket-simonw-1')",11 'call().put_user_policy(PolicyDocument=\'{"Version": "2012-10-17", "Statement": [{"Sid": "ListObjectsInBucket", "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1"]}, {"Sid": "AllObjectActions", "Effect": "Allow", "Action": "s3:*Object", "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/*"]}]}\', PolicyName=\'s3.read-write.pytest-bucket-simonw-1\', UserName=\'s3.read-write.pytest-bucket-simonw-1\')',12 "call().create_access_key(UserName='s3.read-write.pytest-bucket-simonw-1')",13 "call().create_access_key().__getitem__('AccessKey')",14 "call().create_access_key().__getitem__().__str__()",15 ]I used the trick I describe in How to cheat at unit tests with pytest and Black where I run that comparison against an empty [] list, then use pytest --pdb to drop into a debugger and copy and paste the output of [str(c) for c in boto3.mock_calls] into my test code.
Initially I used a comparison directly against boto3.mock_calls - but this threw a surprising error. The calls sequence I baked into my tests looked like this:
1from unittest.mock import call2
3# ...4
5 assert boto3.mock_calls == [6 call("s3"),7 call("iam"),8 call().head_bucket(Bucket="pytest-bucket-simonw-1"),9 call().get_user(UserName="s3.read-write.pytest-bucket-simonw-1"),10 call().put_user_policy(11 PolicyDocument='{"Version": "2012-10-17", "Statement": [{"Sid": "ListObjectsInBucket", "Effect": "Allow", "Action": ["s3:ListBucket"], "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1"]}, {"Sid": "AllObjectActions", "Effect": "Allow", "Action": "s3:*Object", "Resource": ["arn:aws:s3:::pytest-bucket-simonw-1/*"]}]}',12 PolicyName="s3.read-write.pytest-bucket-simonw-1",13 UserName="s3.read-write.pytest-bucket-simonw-1",14 ),15 call().create_access_key(UserName="s3.read-write.pytest-bucket-simonw-1"),16 call().create_access_key().__getitem__("AccessKey"),17 call().create_access_key().__getitem__().__str__(),18 ]But when I ran pytest that last one failed:
1E - 'call().create_access_key().__getitem__()',2E ? - ^3E + call().create_access_key().__getitem__().__str__(),4E ? ^^^^^^^^^^It turns out __str__() calls do not play well with the call() constructor - see this StackOverflow question.
My solution was to cast them all to str() using a list comprehension, which ended up fixing that problem.
Gotcha: parameter ordering#
There’s one major flaw to the str() trick I’m using here: the order in which parameters are displayed in the string representation of call() may differ between Python versions. I had to undo this trick in one place I was using it (see here) as a result due to the following test failure:
1E At index 4 diff:2 "call().get_user_policy(PolicyName='policy-one', UserName='one')"3 != "call().get_user_policy(UserName='one', PolicyName='policy-one')"