So this involves a maybe unusual chain of things:
A.py
from B import call
def make_call():
print("I'm making a call!")
call(phone_number="867-5309")
B.py
def call(phone_number):
pass
test_A.py
import pytest
from A import make_call
@pytest.fixture
def patch_A_call(monkeypatch):
number = "NOT ENTERED"
number_list = []
def call(phone_number):
nonlocal number
number = phone_number
number_list.append(phone_number)
monkeypatch.setattr("A.call", call)
return (number, number_list)
def test_make_call(patch_A_call):
make_call()
print(f"number: {patch_A_call[0]}")
print(f"number_list: {patch_A_call[1]}")
What's printed is:
number: NOT ENTERED
number_list: [867-5309]
I expected "867-5309" to be the value for both results.
I know that lists are passed by reference in Python—but I assumed that the nonlocal
declaration would pass the variable along down the chain.
Why doesn't it work this way?
If you want to see how call
is called, I think a simpler option is to replace it with a Mock object:
from unittest import mock
from A import make_call
@mock.patch('A.call')
def test_make_call(fake_call):
make_call()
assert fake_call.call_args.kwargs['phone_number'] == '867-5309'
Here, we're replacing A.call
with a unittest.mock.Mock
object. This gets called by make_call
, and the call arguments are recorded by the Mock object for later inspection.
This requires substantially less code.
Note that I'm using an assert
statement here, but you could instead print out or otherwise record the value of phone_number
if that's your goal.
The primary problem with your solution is that your patch_A_call
fixture is called once, before your test_make_call
method executes.
So while the nonlocal
keyword is working as intended...you never see the result, because that return (number, number_list)
statement ran before you made the call to make_call
.
You see the result in the list because a list is a "container" -- you add the number to it when calling make_call
, and you can see the result because the returned list is the same object available from inside your patched call
method.
For my solution, we don't have to use mock.patch()
; we can rewrite your fixture like this:
import pytest
from unittest import mock
from A import make_call
@pytest.fixture
def patch_A_call(monkeypatch):
call_recorder = mock.Mock()
monkeypatch.setattr("A.call", call_recorder)
return call_recorder
def test_make_call(patch_A_call):
make_call()
assert patch_A_call.call_args.kwargs["phone_number"] == "867-5309"
This accomplishes pretty much the same thing.