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


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 - з точки зору того, щоб ви зрозуміли, що саме ви тестуєте, - безумовно, повинно бути вашим першим моментом.

Але якщо ваше рішення призведе вас до напрямку розбиття констант, це залишає цікаву частину вашого питання (перефразовуючи) "Чому я повинен торгувати дублюючими константами для дублювання коду?". (Посилаючись на те, де ви говорите про "дублікат [ing] кроку out_dir + ім'я".)

Я вважаю , що ( по модулю коментарів Еріка) в більшості ситуацій зробити вигоду від видалення дублікатів констант. Але робити це потрібно так, щоб не дублювати код. У вашому конкретному прикладі це легко. Замість того, щоб мати шлях до "сировинних" рядків у виробничому коді, трактуйте шлях як шлях. Це більш надійний спосіб приєднати компоненти контуру, ніж з'єднання рядків:

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

З іншого боку, у вашому тестовому коді я б рекомендував щось подібне. Тут наголос показує, що у вас є шлях, і ви "підключаєте" назву файла листа:

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

Тобто, більш продуманим підбором мовних елементів можна автоматично уникнути дублювання коду. (Не весь час, але дуже часто мій досвід.)


1

Я погоджуюся з відповіддю Еріка Ейдта, але є й третій варіант: заглушити константу в тесті, тому тест проходить, навіть якщо змінити значення константи у виробничому коді.

(див. заглушка константи в python unittest )

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заяву назад у тест тому воно проходить знову.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.