Как получить тесты DRYer для простой функции, используя python glob? - PullRequest
0 голосов
/ 15 апреля 2020

У меня есть функция, которая ищет файл в текущем месте, затем в папке загрузки и, если не найден, вызывает и выдает ошибку. Используя pytest и pytest-mock, я смог протестировать код с кодом, намного большим, чем протестированный. Есть ли способ сделать это покрепче / сухим?

Протестированный код:

# cei.py
import glob
import os

def get_xls_filename() -> str:
    """ Returns first xls filename in current folder or Downloads folder """
    csv_filenames = glob.glob("InfoCEI*.xls")
    if csv_filenames:
        return csv_filenames[0]
    home = os.path.expanduser("~")
    csv_filenames = glob.glob(home + "/Downloads/InfoCEI*.xls")
    if csv_filenames:
        return csv_filenames[0]
    return sys.exit(
        "Error: file not found."
    )

Здесь есть три тестовых сценария ios. Найдено в текущем, найдено в загрузках и не найдено. Тестовый код:

# test_cei.py
from unittest.mock import Mock

import pytest
from pytest_mock import MockFixture

import cei

@pytest.fixture
def mock_glob_glob_none(mocker: MockFixture) -> Mock:
    """Fixture for mocking glob.glob."""
    mock = mocker.patch("glob.glob")
    mock.return_value = []
    return mock

@pytest.fixture
def mock_os_path_expanduser(mocker: MockFixture) -> Mock:
    """Fixture for mocking os.path.expanduser."""
    mock = mocker.patch("os.path.expanduser")
    mock.return_value = "/home/user"
    return mock

def test_get_xls_filename_not_found(mock_glob_glob_none, mock_os_path_expanduser) -> None:
    with pytest.raises(SystemExit):
        assert cei.get_xls_filename()
        mock_glob_glob_none.assert_called()
        mock_os_path_expanduser.assert_called_once()

@pytest.fixture
def mock_glob_glob_found(mocker: MockFixture) -> Mock:
    """Fixture for mocking glob.glob."""
    mock = mocker.patch("glob.glob")
    mock.return_value = ["/my/path/InfoCEI.xls"]
    return mock

def test_get_xls_filename_current_folder(mock_glob_glob_found) -> None:
    assert cei.get_xls_filename() == "/my/path/InfoCEI.xls"
    mock_glob_glob_found.assert_called_once()

@pytest.fixture
def mock_glob_glob_found_download(mocker: MockFixture) -> Mock:
    """Fixture for mocking glob.glob."""
    values = {
        "InfoCEI*.xls": [],
        "/home/user/Downloads/InfoCEI*.xls": ["/home/user/Downloads/InfoCEI.xls"],
    }

    def side_effect(arg):
        return values[arg]
    mock = mocker.patch("glob.glob")
    mock.side_effect = side_effect
    return mock


def test_get_xls_filename_download_folder(
    mock_glob_glob_found_download, mock_os_path_expanduser
) -> None:
    assert cei.get_xls_filename() == "/home/user/Downloads/InfoCEI.xls"
    mock_os_path_expanduser.assert_called_once()
    mock_glob_glob_found_download.assert_called_with(
        "/home/user/Downloads/InfoCEI*.xls"
    )

1 Ответ

1 голос
/ 15 апреля 2020

Это, очевидно, немного основано на мнении, но я попробую.

Во-первых, нет ничего плохого в том, что тесты больше тестируемого кода. Это может легко произойти в зависимости от количества протестированных вариантов использования, и я бы не стал использовать этот критерий для качества теста.

При этом ваши тесты обычно должны тестировать API / интерфейс, который в этом случае является возвращаемым путем к файлу при других условиях. Проверка того, был ли вызван os.path.expanduser, является частью внутренней реализации, которая может быть нестабильной - я не считаю это хорошей идеей, по крайней мере, в этом случае. Вы уже тестировали наиболее подходящие варианты использования (может быть добавлен тест на наличие файлов в обоих местах), где используются эти внутренние компоненты.

Вот что я, вероятно, сделал бы:

import os
import pytest
from cei import get_xls_filename


@pytest.fixture
def cwd(fs, monkeypatch):
    fs.cwd = "/my/path"
    monkeypatch.setenv("HOME", "/home/user")


def test_get_xls_filename_not_found(fs, cwd) -> None:
    with pytest.raises(SystemExit):
        assert get_xls_filename()


def test_get_xls_filename_current_folder(fs, cwd) -> None:
    fs.create_file("/my/path/InfoCEI.xls")
    assert get_xls_filename() == "InfoCEI.xls"  # adapted to your implementation


def test_get_xls_filename_download_folder(fs, cwd) -> None:
    path = os.path.join("/home/user", "Downloads", "InfoCEI.xls")
    fs.create_file(path)
    assert get_xls_filename() == path

Обратите внимание, что я использовал приспособление pyfakefs fs, чтобы высмеивать fs (я являюсь участником pyfakefs, так что это то, к чему я привык, и это делает код немного короче), но это может быть излишним для вы.

По сути, я пытаюсь протестировать только API, помещаю общую настройку (здесь cwd и местоположение домашнего пути) в прибор (или в метод настройки для тестов, подобных xUnit), и добавляю спецификацию теста c настройка (создание файла теста) для самого теста.

...