Дублирование констант между тестами и рабочим кодом?

20

Хорошо или плохо дублировать данные между тестами и реальным кодом? Например, предположим, у меня есть класс Python, FooSaverкоторый сохраняет файлы с определенными именами в заданный каталог:

class FooSaver(object):
  def __init__(self, out_dir):
    self.out_dir = out_dir

  def _save_foo_named(self, type_, name):
    to_save = None
    if type_ == FOOTYPE_A:
      to_save = make_footype_a()
    elif type == FOOTYPE_B:
      to_save = make_footype_b()
    # etc, repeated
    with open(self.out_dir + name, "w") as f:
      f.write(str(to_save))

  def save_type_a(self):
    self._save_foo_named(a, "a.foo_file")

  def save_type_b(self):
    self._save_foo_named(b, "b.foo_file")

Теперь в моем тесте я хочу убедиться, что все эти файлы были созданы, поэтому я хочу сказать что-то вроде этого:

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

self.assertTrue(os.path.isfile("/tmp/special_name/a.foo_file"))
self.assertTrue(os.path.isfile("/tmp/special_name/b.foo_file"))

Хотя это дублирует имена файлов в двух местах, я думаю, что это хорошо: это заставляет меня записывать именно то, что я ожидаю получить на другом конце, оно добавляет уровень защиты от опечаток и в целом дает мне уверенность в том, что все работает именно так, как я ожидаю. Я знаю , что если я изменю a.foo_fileк type_a.foo_fileв будущем я буду иметь , чтобы сделать некоторые поиска и замены в моих тестах, но я не думаю , что это слишком большой сделки. Я предпочел бы получить несколько ложных срабатываний, если я забуду обновить тест в обмен на то, чтобы убедиться, что мое понимание кода и тестов синхронизировано.

Коллега считает это дублирование плохим и рекомендовал мне провести рефакторинг обеих сторон примерно так:

class FooSaver(object):
  A_FILENAME = "a.foo_file"
  B_FILENAME = "b.foo_file"

  # as before...

  def save_type_a(self):
    self._save_foo_named(a, self.A_FILENAME)

  def save_type_b(self):
    self._save_foo_named(b, self.B_FILENAME)

и в тесте:

self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.A_FILENAME))
self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.B_FILENAME))

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

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

Здесь есть явный прецедент? Можно ли дублировать константы между тестами и рабочим кодом или они слишком хрупкие?

Патрик Коллинз
источник

Ответы:

16

Я думаю, что это зависит от того, что вы пытаетесь проверить, что соответствует контракту класса.

Если контракт класса именно то, что FooSaverгенерирует a.foo_fileи b.foo_fileв определенном месте, то вы должны проверить это непосредственно, то есть дублировать константы в тестах.

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

Таким образом, вы должны спорить со своим коллегой об истинной природе и контракте класса с точки зрения проектирования доменов более высокого уровня. Если вы не можете согласиться, я бы сказал, что это проблема уровня понимания и абстракции самого класса, а не его тестирования.

Также разумно обнаружить, что контракт класса изменяется во время рефакторинга, например, для повышения уровня его абстракции с течением времени. Во-первых, это могут быть два конкретных файла в определенном временном местоположении, но со временем вы можете обнаружить, что необходима дополнительная абстракция. В это время измените тесты, чтобы синхронизировать их с контрактом класса. Нет необходимости перестраивать контракт класса сразу же, потому что вы его тестируете (YAGNI).

Когда контракт класса недостаточно четко определен, его тестирование может заставить нас усомниться в природе класса, но при этом использовать его. Я бы сказал, что вам не следует обновлять контракт класса только потому, что вы его тестируете; Вы должны обновить контракт класса по другим причинам, например, это слабая абстракция для домена, а если нет, то протестируйте его как есть.

Эрик Эйдт
источник
4

То, что предложил @Erik - чтобы убедиться, что вы четко понимаете, что именно вы тестируете - должно быть вашим первым соображением.

Но должно ли ваше решение привести вас к направлению выделения констант, что оставляет интересную часть вашего вопроса (перефразируя) «Почему я должен обменивать дублирующие константы на дублирующий код?». (В отношении того, где вы говорите о «дублировании шага out_dir + name».)

Я считаю , что ( по модулю комментариев Эрика) в большинстве ситуаций сделать выгоду от удаления дубликатов констант. Но вы должны сделать это так, чтобы не дублировать код. В вашем конкретном примере это легко. Вместо того, чтобы иметь дело с путем как «сырыми» строками в вашем производственном коде, трактуйте путь как путь. Это более надежный способ объединения компонентов пути, чем конкатенация строк:

os.path.join(self.out_dir, name)

С другой стороны, в вашем тестовом коде я бы порекомендовал что-то подобное. Здесь акцент делается на том, что у вас есть путь, и вы «подключаете» имя конечного файла:

self.assertTrue(os.path.isfile("/tmp/special_name/{0}".format(FooSaver.A_FILENAME)))

То есть, благодаря более продуманному выбору языковых элементов, вы можете автоматически избежать дублирования кода. (Не всегда, но очень часто по моему опыту.)

Майкл Соренс
источник
1

Я согласен с ответом Эрика Эйдта, но есть и третий вариант: удалить константу в тесте, чтобы тест прошел, даже если вы измените значение константы в рабочем коде.

(см. заглушение константы в тесте Python )

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

with mock.patch.object(FooSaver, 'A_FILENAME', 'unique_to_your_test_a'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_a"))
with mock.patch.object(FooSaver, 'B_FILENAME', 'unique_to_your_test_b'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_b"))

И когда я делаю что-то подобное, я обычно withпровожу проверку работоспособности, где я запускаю тесты без оператора и проверяю, что я вижу «a.foo_file»! = «Unique_to_your_test_a» », а затем помещаю withоператор обратно в тест. так что снова проходит.

alexanderbird
источник