Издевательская конструкция класса
Я только начинаю использовать библиотеку 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
позже. Если бы это было невозможно, я бы оставил его как есть. Если ваши шесть строк макет кода являются единственной частью тестовых случаев, о которых вы беспокоитесь, тогда вы в порядке.