Mocking components and responses


#1

This might be a bit general, but how does one fool apistar with mocked objects?

For example, I have a current test that mocks an object to trap a request that gets sent upstream.

@patch('wideband.lib.address.WidebandAddress.call')
def test_address_autocomplete(mock_call):
    mock_call.return_value = {'My result'}
    address = WidebandAddress(token=Mock(token='mytoken'))
    data = 'search'
    response = address.autocomplete(data)
    assert response == {'My result'}
    mock_call.assert_called_once_with('post', 'https://api.wideband.net.au/sandpit/v2/nbn/address/autocomplete',
                                      headers={'Content-Type': 'application/json', 'Authorization': 'Bearer mytoken'},
                                      json={'search': data})

However, I now realise I’m doing myself a disservice by not using the TestClient, but I still want to mock my upstream call.

@patch('wideband.lib.wideband.Wideband')
@patch('wideband.lib.address.WidebandAddress.call')
def test_address_autocomplete(
        mock_call,
        mock_wideband
):
    mock_call.return_value = {'My result'}
    mock_wideband.address = WidebandAddress(token='mytoken')
    response = client.post('/nbn/address/autocomplete', json={'search': 'search'}, headers=headers)
    assert response.status_code == 200
    assert response.json() == {'My result'}
    mock_call.assert_called_once_with('post', 'https://api.wideband.net.au/sandpit/v2/nbn/address/autocomplete',
                                      headers={'Content-Type': 'application/json', 'Authorization': 'Bearer mytoken'},
                                      json={'search': 'search'})

When I run the code above, I get AttributeError: type object 'MagicMock' has no attribute '_JSONResponse__name_'

Under the hood, I’m using requests, so perhaps there’s something I need to be returning in particular from my call function that I’m mocking, rather than a plain dict.


#2

After having this rattling around in my head over the weekend, I’ve worked it out. requests-mock to the rescue!

The trick was to allow “real” requests go through, such as the request to the testserver, but then catch my token generation, and then the actual request I wanted to make sure happened.

Using the additional_matcher, I can also assert that the content type and mocked token is looking as expected.

For completeness, here’s a sample of my test code, minus the function that generates a test token.

headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer mytoken'}

def header_matcher(request):
    assert request.headers['Content-Type'] == 'application/json'
    assert request.headers['Authorization'] == 'Bearer mytoken'
    return True


def test_address_autocomplete(requests_mock):

    def request_matcher(request):
        assert request.json() == {'search': 'search'}
        return header_matcher(request)

    hits = {
        "hits": [ # Gutted for brevity
        ]
    }
    requests_mock.post('http://testserver/nbn/address/autocomplete', real_http=True)
    requests_mock.post('https://api.wideband.net.au/sandpit/v1/token', json=generate_wideband_token())
    requests_mock.post(
        'https://api.wideband.net.au/sandpit/v2/nbn/address/autocomplete',
        additional_matcher=request_matcher,
        json=hits)
    response = client.post('/nbn/address/autocomplete', json={'search': 'search'}, headers=headers)
    assert response.status_code == 200
    assert response.json() == hits