Лучшие практики использования макета Python для тестирования функций в подмодулях

Итак, представьте, что у меня есть простая библиотека, для которой я пытаюсь написать модульные тесты. Эта библиотека взаимодействует с базой данных, а затем использует эти данные для вызова SOAP API. У меня есть три модуля и тестовый файл для каждого модуля.

структура каталога:

./mypkg
    ../__init__.py
    ../main.py
    ../db.py
    ../api.py

./tests
    ../test_main
    ../test_db
    ../test_api

Код:

#db.py
import mysqlclient
class Db(object):
    def __init__(self):
        self._client = mysqlclient.Client()

    @property
    def data(self):
        return self._client.some_query()


#api.py
import soapclient
class Api(object):
    def __init__(self):
        self._client = soapclient.Client()

    @property
    def call(self):
        return self._client.some_external_call()


#main.py
from db import Db
from api import Api

class MyLib(object):
    def __init__(self):
        self.db = Db()
        self.api = Api()

    def caller(self):
        return self.api.call(self.db.data)

Юнит-тесты:

#test_db.py
import mock
from mypkg.db import Db

@mock.patch('mypkg.db.mysqlclient')
def test_db(mysqlclient_mock):
    mysqlclient_mock.Client.return_value.some_query = {'data':'data'}
    db = Db()
    assert db.data == {'data':'data'}


#test_api.py
import mock
from mypkg.api import Api

@mock.patch('mypkg.db.soapclient')
def test_db(soap_mock):
    soap_mock.Client.return_value.some_external_call = 'foo'
    api = Api()
    assert api.call == 'foo'

В приведенном выше примере mypkg.main.MyLib вызывает mypkg.db.Db() (использует сторонний mysqlclient), а затем mypkg.api.Api() (использует сторонний soapclient).

Я использую mock.patch для исправления сторонних библиотек, чтобы имитировать мои вызовы db и API в test_db и test_api по отдельности.

Теперь мой вопрос: рекомендуется ли снова исправлять эти внешние вызовы в test_main ИЛИ просто исправлять db.Db и api.Api? (этот пример довольно прост, но в больших библиотеках код становится громоздким при повторном исправлении внешних вызовов или даже при использовании тестовых вспомогательных функций, которые исправляют внутренние библиотеки).

Вариант 1: снова пропатчить внешние библиотеки в main

#test_main.py
import mock
from mypkg.main import MyLib

@mock.patch('mypkg.db.mysqlclient')
@mock.patch('mypkg.api.soapclient')
def test_main(soap_mock, mysqlcient_mock):
    ml = MyLib()
    soap_mock.Client.return_value.some_external_call = 'foo'
    assert ml.caller() == 'foo'

Вариант 2: пропатчить внутренние библиотеки

#test_main.py
import mock
from mypkg.main import MyLib

@mock.patch('mypkg.db.Db')
@mock.patch('mypkg.api.Api')
def test_main(api_mock, db_mock):
    ml = MyLib()
    api_mock.return_value = 'foo'
    assert ml.caller() == 'foo'

person abarik    schedule 22.08.2017    source источник


Ответы (1)


mock.patch создает фиктивную версию чего-либо там, где оно импортировано, а не там, где оно находится. Это означает, что строка, переданная в mock.patch, должна быть путем к импортированному модулю в тестируемом модуле. Вот как должны выглядеть декораторы патчей в test_main.py:

@mock.patch('mypkg.main.Db')
@mock.patch('mypkg.main.Api')

Кроме того, дескрипторы ваших пропатченных модулей (api_mock и db_mock) относятся к классам, а не экземплярам этих классов. Когда вы пишете api_mock.return_value = 'foo', вы говорите api_mock возвращать 'foo', когда он вызывается, а не когда у его экземпляра вызывается метод. Вот объекты в main.py и то, как они связаны с api_mock и db_mock в вашем тесте:

Api is a class                     : api_mock
Api() is an instance               : api_mock.return_value
Api().call is an instance method   : api_mock.return_value.call
Api().call() is a return value     : api_mock.return_value.call.return_value

Db is a class                      : db_mock
Db() is an instance                : db_mock.return_value
Db().data is an attribute          : db_mock.return_value.data

Таким образом, test_main.py должен выглядеть так:

import mock
from mypkg.main import MyLib

@mock.patch('mypkg.main.Db')
@mock.patch('mypkg.main.Api')
def test_main(api_mock, db_mock):
    ml = MyLib()

    api_mock.return_value.call.return_value = 'foo'
    db_mock.return_value.data = 'some data' # we need this to test that the call to api_mock had the correct arguments.

    assert ml.caller() == 'foo'
    api_mock.return_value.call.assert_called_once_with('some data')

Первый патч в Варианте 1 отлично подойдет для модульного тестирования db.py, потому что он дает модулю db фиктивную версию mysqlclient. Точно так же @mock.patch('mypkg.api.soapclient') принадлежит test_api.py.

Я не могу придумать, как вариант 2 может помочь вам в модульном тестировании чего-либо.

Отредактировано: я неправильно называл классы модулями. db.py и api.py — это модули

person lortimer    schedule 23.08.2017