Проблема с издевательством над pathlib.Path.is_dir()

#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
 

Как вы можете видеть, побочный эффект также работает и сейчас.