Home > Back-end >  Pytest: Asserting multiple calls on a function with a dictionary parameter
Pytest: Asserting multiple calls on a function with a dictionary parameter

Time:04-09

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})
    ]
  • Related