I want to test a python snippet which is requesting information from a paginated API and updates the offset if there is more information available.
Some minimal example code:
def fetch(url, params):
# call the API
def fetch_all():
params = {"offset": 0}
while True:
result = fetch("example.com", params).json()
if len(result) > 1000: # If there are more items
params["offset"] = 1
else:
break
While testing this function, I am mocking the fetch
function and checking if it has multiple calls with different offsets. Unfortunately, all those calls seem to reference the same dictionary .
Therefore if I iterate though it e.g. 3 times, it looks like this:
Expected:
call("example.com", {"offset": 0}),
call("example.com", {"offset": 1}),
call("example.com", {"offset": 2})
Actual:
call("example.com", {"offset": 2}),
call("example.com", {"offset": 2}),
call("example.com", {"offset": 2})
If I instead change the code to result = fetch("example.com", params.copy())
, it works properly. So the calls recorded in the mock definitely seem to reference the same dictionary.
How do I fix it? I do not really want to always have to hand in copies only to be able to do some testing on it.
-- Edit --
I've put together a working example (called test.py
) that shows the stated behavior:
def fetch(url: str, params: dict):
pass # This is getting mocked
def fetch_all():
params = {"offset": 0}
result = []
while True:
data = fetch("https://example.com", params)
result.extend(data)
if len(data) >= 5:
params["offset"] = 1
else:
break
return result
def test_fetch_all(mocker):
side_effect = [
[1,2,3,4,5],
[6,7]
]
mocked_fetch = mocker.patch("test.fetch", side_effect=side_effect)
assert fetch_all() == [1,2,3,4,5,6,7]
mocked_fetch.assert_has_calls([
mocker.call("https://example.com", params={"offset": 0}),
mocker.call("https://example.com", params={"offset": 1})
])
Resulting in:
AssertionError: Calls not found.
Expected: [call('https://example.com', params={'offset': 0}),
call('https://example.com', params={'offset': 1})]
Actual: [call('https://example.com', {'offset': 1}),
call('https://example.com', {'offset': 1})]
CodePudding user response:
You can create a stub method and store a copy of the parameters every time the method is called, then assert against your stored copy.
I made a few modifications to your working example to make it work:
def test_fetch_all(mocker):
return_values = [
[1, 2, 3, 4, 5],
[6, 7]
]
call_parameters = []
def my_fetch(url: str, params: dict):
call_parameters.append((url, params.copy()))
return return_values.pop(0)
mocker.patch("test.fetch", side_effect=my_fetch)
assert fetch_all() == [1, 2, 3, 4, 5, 6, 7]
assert call_parameters == [
("https://example.com", {"offset": 0}),
("https://example.com", {"offset": 1})
]