#python #mocking #pytest
Вопрос:
У меня есть код, который проверяет, является ли указанный файл каталогом:
from pathlib import Path
def simple_method():
to_check = ['/tmp', '/tmp/test']
print("")
for element in to_check:
path = Path(element)
print(path)
result = path.is_dir()
print(f"#MBB SC: {dir} -> {result}")
if result:
print(f"{element} is a dir")
else:
print(f"{element} is not a dir")
Я хочу написать для него тест. В тестовом сценарии я хочу имитировать pathlib.Path.is_dir() и установить первый вызов is_dir (для /tmp) в значение True, а второй (для /tmp/test) в значение False. Вот как я хотел это решить:
import pytest
from unittest.mock import call, patch, MagicMock
import source_code
class TestSourceCode():
def test(self):
source_code.Path = MagicMock()
source_code.Path.is_dir = MagicMock()
source_code.Path.is_dir.side_effect = [True, False]
source_code.simple_method()
assert source_code.Path.call_args_list == [call('/tmp'), call('/tmp/test')]
print(f"n")
print(f"source_code.Path")
print(f"source_code.Path.call_count {source_code.Path.call_count}")
print(f"source_code.Path.call_args {source_code.Path.call_args}")
print(f"source_code.Path.method_calls {source_code.Path.method_calls}")
print(f"source_code.Path.call_args_list {source_code.Path.call_args_list}")
print(f"n")
print(f"source_code.Path.is_dir")
print(f"source_code.Path.is_dir.call_count {source_code.Path.is_dir.call_count}")
print(f"source_code.Path.is_dir.call_args {source_code.Path.is_dir.call_args}")
print(f"source_code.Path.is_dir.method_calls {source_code.Path.is_dir.method_calls}")
print(f"source_code.Path.is_dir.call_args_list {source_code.Path.is_dir.call_args_list}")
После запуска pytest я получаю вывод:
test_code.py
<MagicMock name='mock()' id='4278575696'>
#MBB SC: <built-in function dir> -> <MagicMock name='mock().is_dir()' id='4278615568'>
/tmp is a dir
<MagicMock name='mock()' id='4278575696'>
#MBB SC: <built-in function dir> -> <MagicMock name='mock().is_dir()' id='4278615568'>
/tmp/test is a dir
source_code.Path
source_code.Path.call_count 2
source_code.Path.call_args call('/tmp/test')
source_code.Path.method_calls []
source_code.Path.call_args_list [call('/tmp'), call('/tmp/test')]
source_code.Path.is_dir
source_code.Path.is_dir.call_count 0
source_code.Path.is_dir.call_args None
source_code.Path.is_dir.method_calls []
source_code.Path.is_dir.call_args_list []
Он распознает два ложных вызова пути, но не видит издевательских вызовов is_dir. Более того, он всегда устанавливает результат как истинный. Я знаю, что это потому, что результат-это макет объекта. Мне интересно, почему результат не является возвращаемым значением для этого макета. Что я делаю не так в этом тесте?
Ответ №1:
Вы вызываете is_dir
объект, а не класс, поэтому вам нужно использовать возвращаемое значение Path
макета. Кроме того, вызов уже возвращает макет (как и каждый вызов макета), поэтому нет необходимости заменять его. Вот рабочая версия (я использовал пару переменных, чтобы сделать ее более читаемой):
class TestSourceCode:
def test(self):
path_mock = source_code.Path = MagicMock()
is_dir_mock = path_mock.return_value.is_dir
is_dir_mock.side_effect = [True, False]
source_code.simple_method()
assert path_mock.call_count == 2
assert path_mock.call_args_list == [call('/tmp'), call('/tmp/test')]
assert is_dir_mock.call_count == 2
assert is_dir_mock.call_args_list == [call(), call()]
Это дает вам
$ python -m pytest -s
...
<MagicMock name='mock()' id='1399508012056'>
#MBB SC: <built-in function dir> -> True
/tmp is a dir
<MagicMock name='mock()' id='1399508012056'>
#MBB SC: <built-in function dir> -> False
/tmp/test is not a dir
Как вы можете видеть, побочный эффект также работает и сейчас.