Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to use a fixture in another fixture and both in a test?

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.

like image 536
Wayne Werner Avatar asked Oct 04 '17 14:10

Wayne Werner


People also ask

Can fixtures use other fixtures?

A fixture can use multiple other fixtures. Just like a test method can take multiple fixtures as arguments, a fixture can take multiple other fixtures as arguments and use them to create the fixture value that it returns.

Can we use multiple fixtures in pytest?

Fixtures can also be requested more than once during the same test, and pytest won't execute them again for that test. This means we can request fixtures in multiple fixtures that are dependent on them (and even again in the test itself) without those fixtures being executed more than once.

Which decorator is used for tests using multiple fixtures?

Parametrized tests The above decorator is a very powerful functionality, it permits to call a test function multiple times, changing the parameters input at each iteration.

How many times will a fixture of class scope run for the tests of a class?

Class: With Class scope, one fixture will be created per class object. Session: With the Session scope, the fixture will be created only once for entire test session.


1 Answers

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.

like image 111
Samuel Dion-Girardeau Avatar answered Sep 19 '22 13:09

Samuel Dion-Girardeau