7

I'm mocking out an API using unittest.mock. My interface is a class that uses requests behind the scene. So I'm doing something like this:

@pytest.fixture
def mocked_api_and_requests():
    with mock.patch('my.thing.requests') as mock_requests:
        mock_requests.post.return_value = good_credentials
        api = MyApi(some='values', that='are defaults')
        yield api, mock_requests


def test_my_thing_one(mocked_api_and_requests):
    api, mocked_requests = mocked_api_and_requests
    ...  # some assertion or another

def test_my_thing_two(mocked_api_and_requests):
    api, mocked_requests = mocked_api_and_requests
    ... # some other assertions that are different

As you can probably see, I've got the same first line in both of those tests and that smells like it's not quite DRY enough for me.

I'd love to be able to do something like:

def test_my_thing_one(mock_requests, logged_in_api):
    mock_requests.get.return_value = ...

Rather than have to unpack those values, but I'm not sure if there's a way to reliably do that using pytest. If it's in the documentation for fixtures I've totally missed it. But it does feel like there should be a right way to do what I want to do here.

Any ideas? I'm open to using class TestGivenLoggedInApiAndMockRequests: ... if I need to go that route. I'm just not quite sure what the appropriate pattern is here.

Wayne Werner
  • 49,299
  • 29
  • 200
  • 290

2 Answers2

1

It is possible to achieve exactly the result you want by using multiple fixtures.

Note: I modified your example minimally so that the code in my answer is self-contained, but you should be able to adapt it to your use case easily.

In myapi.py:

import requests

class MyApi:

    def get_uuid(self):
        return requests.get('http://httpbin.org/uuid').json()['uuid']

In test.py:

from unittest import mock
import pytest
from myapi import MyApi

FAKE_RESPONSE_PAYLOAD = {
    'uuid': '12e77ecf-8ce7-4076-84d2-508a51b1332a',
}

@pytest.fixture
def mocked_requests():
    with mock.patch('myapi.requests') as _mocked_requests:
        response_mock = mock.Mock()
        response_mock.json.return_value = FAKE_RESPONSE_PAYLOAD
        _mocked_requests.get.return_value = response_mock
        yield _mocked_requests

@pytest.fixture
def api():
    return MyApi()

def test_requests_was_called(mocked_requests, api):
    assert not mocked_requests.get.called
    api.get_uuid()
    assert mocked_requests.get.called

def test_uuid_is_returned(mocked_requests, api):
    uuid = api.get_uuid()
    assert uuid == FAKE_RESPONSE_PAYLOAD['uuid']

def test_actual_api_call(api):  # Notice we don't mock anything here!
    uuid = api.get_uuid()
    assert uuid != FAKE_RESPONSE_PAYLOAD['uuid']

Instead of defining one fixture that returns a tuple, I defined two fixtures, which can independently be used by the tests. An advantage of composing fixtures like that is that they can be used independently, e.g. the last test actually calls the API, simply by virtue of not using mock_requests fixture.

Note that -- to answer the question title directly -- you could also make mocked_requests a prerequisite of the api fixture by simply adding it to the parameters, like so:

@pytest.fixture
def api(mocked_requests):
    return MyApi()

You will see that it works if you run the test suite, because test_actual_api_call will no longer pass.

If you make this change, using the api fixture in a test will also mean executing it in the context of mocked_requests, even if you don't directly specify the latter in your test function's arguments. It's still possible to use it explicitly, e.g. if you want to make assertions on the returned mock.

Samuel Dion-Girardeau
  • 2,790
  • 1
  • 29
  • 37
0

If you can not afford to easily split your tuple fixture into two independent fixtures, you can now "unpack" a tuple or list fixture into other fixtures using my pytest-cases plugin as explained in this answer.

Your code would look like:

from pytest_cases import pytest_fixture_plus

@pytest_fixture_plus(unpack_into="api,mocked_requests")
def mocked_api_and_requests():
    with mock.patch('my.thing.requests') as mock_requests:
        mock_requests.post.return_value = good_credentials
        api = MyApi(some='values', that='are defaults')
        yield api, mock_requests

def test_my_thing_one(api, mocked_requests):
    ...  # some assertion or another

def test_my_thing_two(api, mocked_requests):
    ... # some other assertions that are different
smarie
  • 4,568
  • 24
  • 39
  • Are you sure this work? Using pytest_fixture_plus with yield causes an error # get the required fixture's value (the tuple to unpack) source_fixture_value = kwargs.pop(source_f_name) # unpack: get the item at the right position. > return source_fixture_value[_value_idx] E TypeError: 'generator' object is not subscriptable – Edmondo Aug 25 '20 at 16:53
  • It is supposed to work, yes. Let's discuss on the issue you've opened https://github.com/smarie/python-pytest-cases/issues/130 – smarie Aug 26 '20 at 09:34