Frage Ist es möglich, ein Gerät in einem anderen Gerät und beide in einem Test zu verwenden?


Ich verspotte eine API mit unittest.mock. Meine Schnittstelle ist eine Klasse, die verwendet requests hinter den Kulissen. Also mache ich so etwas:

@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

Wie Sie wahrscheinlich sehen können, habe ich die gleiche erste Zeile in beiden dieser Tests und das riecht, als wäre es nicht ganz DRY genug für mich.

Ich würde gerne etwas tun können wie:

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

Anstatt diese Werte auspacken zu müssen, bin ich mir nicht sicher, ob es einen Weg gibt, das zuverlässig mit Pytest zu machen. Wenn es in der Dokumentation für Fixtures Ich habe es total vermisst. Aber es fühlt sich an, als müsste es einen richtigen Weg geben, um das zu tun, was ich hier machen möchte.

Irgendwelche Ideen? Ich bin offen für die Verwendung class TestGivenLoggedInApiAndMockRequests: ... wenn ich diesen Weg gehen muss. Ich bin mir nicht sicher, was das richtige Muster ist.


5
2017-10-04 14:58


Ursprung


Antworten:


Es ist möglich, mit mehreren Scheinwerfern genau das gewünschte Ergebnis zu erzielen.

Hinweis: Ich habe Ihr Beispiel minimal geändert, sodass der Code in meiner Antwort in sich abgeschlossen ist. Sie sollten ihn jedoch problemlos an Ihren Anwendungsfall anpassen können.

Im myapi.py:

import requests

class MyApi:

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

Im 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']

Anstatt ein Fixture zu definieren, das ein Tupel zurückgibt, habe ich zwei Fixtures definiert, die unabhängig voneinander von den Tests verwendet werden können. Ein Vorteil des Zusammenstellens von Scheinwerfern ist, dass sie unabhängig voneinander verwendet werden können, z. der letzte Test ruft tatsächlich die API auf, einfach weil sie nicht verwendet wird mock_requests Leuchte.

Beachten Sie, dass - um den Fragetitel direkt zu beantworten - Sie auch machen könnten mocked_requests eine Voraussetzung für die api Fixture einfach durch Hinzufügen zu den Parametern, so:

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

Sie werden sehen, dass es funktioniert, wenn Sie die Testsuite ausführen, weil test_actual_api_call wird nicht mehr bestehen.

Wenn Sie diese Änderung vornehmen, verwenden Sie die api Fixture in einem Test bedeutet auch die Ausführung im Kontext von mocked_requests, auch wenn Sie Letzteres nicht direkt in den Argumenten Ihrer Testfunktion angeben. Es ist immer noch möglich, es explizit zu verwenden, z. wenn Sie Aussagen über den zurückgegebenen Schein machen wollen.


1
2018-05-23 00:51