Издевательская конструкция класса

Я только начинаю использовать библиотеку Python mock, чтобы помочь писать более сжатые и изолированные модульные тесты. Моя ситуация в том, что у меня есть класс, который читает данные из довольно волосатого формата, и я хочу протестировать метод в этом классе, который представляет данные в чистом виде Формат.

class holds_data(object):
    def __init__(self, path):
        """Pulls complicated data from a file, given by 'path'.

        Stores it in a dictionary. 
        """
        self.data = {}
        with open(path) as f:
            self.data.update(_parse(f))

    def _parse(self, file):
        # Some hairy parsing code here
        pass

    def x_coords(self):
        """The x coordinates from one part of the data
        """
        return [point[0] for point in self.data['points']]

Приведенный выше код упрощает то, что у меня есть. На самом деле _parse - довольно значительный метод, на котором у меня есть тестовое покрытие на функциональном уровне.

Я бы хотел, однако, проверить x_coords на уровне unit test. Если бы я должен был создать этот класс, указав ему путь, он нарушил бы правила модульных тестов, потому что:

Тест не является unit test, если:

  • Он касается файловой системы

Итак, я хотел бы иметь возможность исправить метод __init__ для holds_data, а затем просто заполнить часть self.data, необходимую для x_coords. Что-то вроде:

from mock import patch
with patch('__main__.holds_data.__init__') as init_mock:
    init_mock.return_value = None
    instance = holds_data()
    instance.data = {'points':[(1,1),(2,2),(3,4)]}
    assert(instance.x_coords == [1,2,3])

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

Изменить: Чтобы быть ясным, моя проблема заключается в том, что во время инициализации мой класс выполняет значительную обработку данных для организации данных, которые будут представлены с помощью метода типа x_coords. Я хочу знать, что является самым простым способом для исправления всех этих шагов, без необходимости предоставления полного примера ввода. Я хочу только проверить поведение x_coords в ситуации, когда я управляю данными, которые он использует.

Мой вопрос о том, есть ли здесь запах кода, сводится к этой проблеме:

Я уверен, что это было бы проще, если бы я реорганизовал, чтобы x_coords была автономной функцией, которая принимает holds_data в качестве параметра. Если "легче тестировать == лучший дизайн", это будет путь. Тем не менее, для функции x_coords требуется знать больше о внутренних элементах holds_data, с которыми мне обычно было бы удобно. Где я должен сделать компромисс? Чистые коды или чистые тесты?

Ответ 1

Так как вы заинтересованы только в тестировании одного метода, почему бы вам просто не издеваться над целым классом HoldsData и вывести на него метод x_coords?

>>> mock = MagicMock(data={'points': [(0,1), (2,3), (4,5)]})
>>> mock.x_coords = HoldsData.__dict__['x_coords']
>>> mock.x_coords(mock)
[0, 2, 4]

Таким образом, вы будете иметь полный контроль над входом и выходом x_coords (либо по побочному эффекту, либо по возврату).

Примечание: В py3k вы можете просто сделать mock.x_coords = HoldsData.x_coords, так как не больше unbound methods.

Это также можно сделать в конструкторе макета:

MagicMock(data={'points': [(0,1), (2,3), (4,5)]}, x_coords=HoldsData.__dict__['x_coords'])

Ответ 2

В основном вы сталкиваетесь с этой проблемой:

Тест не является unit test, если:

  • Он касается файловой системы

Если вы хотите следовать этому правилу, вы должны изменить метод _parse. В частности, он не должен принимать файл в качестве входных данных. Задача _parse заключается в анализе данных, но откуда поступают данные, не является проблемой этого метода.

У вас может быть строка, содержащая те же данные, которые затем передаются в _parse. Точно так же данные могут поступать из базы данных или в другое место. Когда _parse принимает данные только как входные данные, вы можете unit test использовать этот метод намного проще.

Он будет выглядеть примерно так:

class HoldsData(object):
    def __init__(self, path):
        self.data = {}
        file_data = self._read_data_from_file(path)
        self.data.update(self._parse(file_data))

    def _read_data_from_file(self, path):
        # read data from file
        return data

    def _parse(self, data):
        # do parsing

Чистый код ведет к чистоте тестов. Лучшим случаем было бы издеваться над данными и предоставить _parse с помощью ввода, а затем проверить x_coords позже. Если бы это было невозможно, я бы оставил его как есть. Если ваши шесть строк макет кода являются единственной частью тестовых случаев, о которых вы беспокоитесь, тогда вы в порядке.