Python unittest.mock: метод исправленного класса вызывается, но утверждение не выполняется

Я пытаюсь издеваться над несколькими компонентами служебного класса. Хотя assert_called() подходит для одного метода, для другого он не подходит, но я уверен, что вызываются оба. Я использую Python 3.7.3 в Windows 10.

Я сократил свой сценарий до самого необходимого. Класс полезности (util.py):

class api:

    @staticmethod    
    def send(data):
        print("sending %s" % data)

    class logger:

        @staticmethod
        def info(s):
            print("INFO: %s" % s)

        @staticmethod
        def error(s):
            print("ERROR: %s" % s)

Вариант модульного теста, который отлично работает:

import unittest
from unittest.mock import patch
from util import api

def do_something():
    api.logger.info("doing something")
    api.send("some data")

@patch("util.api.logger")
class Test(unittest.TestCase):

    def test_do_something(self, mock_logger):
        do_something()
        mock_logger.info.assert_called()

Тот, который терпит неудачу:

import unittest
from unittest.mock import patch
from util import api

def do_something():
    api.logger.info("doing something")
    api.send("some data")

@patch("util.api")
class Test(unittest.TestCase):

    def test_do_something(self, mock_api):
        do_something()
        mock_api.send.assert_called()

При запуске тестов я получаю оба выхода print():

INFO: doing something
sending some data

поэтому я уверен, что оба метода вызываются.

Скорее всего, я делаю какую-то глупую фиктивную ошибку, потому что я действительно новичок в python...

Еще немного фона:

В моем урезанном сценарии do_something() — это просто замена набора функций, которые являются объектом моего теста и которые в моем реальном сценарии фактически определены в отдельном файле python. В производственной среде он работает в контексте фреймворка, предоставляющего служебный класс API. В моей тестовой среде util.py сам по себе является макетом производственного API. Тестируемый файл py (вместо def do_something(): ...) загружается следующим образом:

path = os.getcwd() + "<local path to py file to be tested>"
globals().update({ **runpy.run_path(path, init_globals=globals()), **globals() })

Поэтому я не могу изменить код do_something() в тестовом сценарии.


person Kai Roesner    schedule 29.05.2019    source источник


Ответы (2)


Это было непросто.

Проблема в том, что класс api импортируется тестовым классом задолго до того, как он будет исправлен. Из документации по patch:

Цель импортируется при выполнении декорированной функции, а не во время декорирования.

Поскольку вы первым делом импортируете класс api в тестовый класс, этот парень не будет исправлен, когда вы запустите тест. Python считает, что он уже импортирован.

Обратите внимание, что это не относится к классу logger, поскольку вы никогда не импортируете его. Если бы вы это сделали, первый тест тоже был бы провален.

Самое простое исправление - перенести импорт внутрь тестовой функции.

def do_something():
     from util import api
     api.logger.info("doing something")
     api.send("some data")

Теперь patch будет выполнять импорт, поэтому макет помещается в тестовую область вместо фактического класса.

person rdas    schedule 29.05.2019
comment
Это устраняет проблему в моем урезанном сценарии. Однако в реальном сценарии я не могу изменить do_something(). Я добавил дополнительную информацию к своему исходному сообщению... - person Kai Roesner; 31.05.2019

Наконец получил его после того, как нашел этот вопрос SO - я не знал, что вы можете также исправлять отдельные функции/методы класса (не упомянутые в ссылка на unittest.mock.patch). Итак, это работает:

import unittest
from unittest.mock import patch
from util import api

def do_something():
    api.logger.info("doing something")
    api.send("some data")

@patch("util.api.send")
@patch("util.api.logger")
class Test(unittest.TestCase):

    def test_do_something(self, mock_logger, mock_send):
        do_something()
        mock_send.assert_called()
        mock_logger.info.assert_called()
person Kai Roesner    schedule 31.05.2019