Хорошо это или плохо, что набор тестов быстрой проверки соответствует реализациям? - PullRequest
0 голосов
/ 21 сентября 2018

Я пытаюсь начать работу с QuickCheck на Haskell, и хотя я знаком с концепциями, лежащими в основе методологии тестирования, я впервые пытаюсь использовать ее в проекте, выходящем за рамки тестирования, например reverse . reverse == id и тому подобное.Я хочу знать, полезно ли применять его к бизнес-логике (я думаю, что это очень возможно).

Итак, пара существующих функций типа бизнес-логики, которые я хотел бы протестировать, выглядит следующим образом:

shouldDiscountProduct :: User -> Product -> Bool
shouldDiscountProduct user product =
  if M.isNothing (userDiscountCode user)
     then False
     else if (productDiscount product) then True
                                       else False

Для этой функции я могу написать спецификацию QuickCheck, например:

data ShouldDiscountProductParams
  = ShouldDiscountProductParams User Product

instance Show ShouldDiscountProductParams where
  show (ShouldDiscountProductParams u p) =
    "ShouldDiscountProductParams:\n\n" <>
    "- " <> show u <> "\n\n" <>
    "- " <> show p

instance Arbitrary ShouldDiscountProductParams where
  arbitrary = ShouldDiscountProductParams <$> arbitrary <*> arbitrary

shouldDiscountProduct :: Spec
shouldDiscountProduct = it behavior (property verify)
  where
    behavior =
      "when product elegible for discount\n"
      <> " and user has discount code"

    verify (ShouldDiscountProductParams p t) =
      subject p t `shouldBe` expectation p t

    subject =
      SUT.shouldDiscountProduct

    expectation User{..} Product{..} =
      case (userDiscountCode, productDiscount) of
        (Just _, Just _) -> True
        _ -> False

И в результате я получаю функцию expectation, которая проверяет текущую реализацию shouldDiscountProduct, просто более элегантно.Итак, теперь у меня есть тест, я могу изменить свою первоначальную функцию.Но моя естественная склонность была бы изменить его на реализацию в expectation:

shouldDiscountProduct User{..} Product{..} =
  case (userDiscountCode, productDiscount) of
    (Just _, Just _) -> True
    _ -> False

Но это нормально, верно?Если я хочу изменить эту функцию в будущем, у меня будет та же самая функция, готовая проверить, что мои изменения являются соответствующими и не случайно нарушают что-либо.

Или это излишняя / двойная бухгалтерия?Я полагаю, что в ходе ООП-тестирования я понял, что вам следует стараться избегать как можно большего отражения деталей реализации, это буквально не может быть дальше, это реализация!

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

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

Ответы [ 3 ]

0 голосов
/ 31 октября 2018

По существу, для проверки свойств имеет смысл сравнивать две реализации одной и той же функции, когда:

  1. Обе функции являются частью API, и каждая из них должна реализовыватьопределенная функция.Например, мы обычно хотим liftEq (==) = (==).Таким образом, мы должны проверить, что liftEq для определяемого нами типа удовлетворяет этому свойству.

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

Для типичной «бизнес-логики» ни одно из них не применимо.Однако могут быть некоторые особые случаи, когда они это делают.Например, у вас могут быть две разные функции, которые вы вызываете при разных обстоятельствах, которые должны согласовываться при определенных условиях.

0 голосов
/ 05 марта 2019

Извините, что прыгнул через несколько месяцев, но, поскольку этот вопрос легко появляется в Google, я думаю, что ему нужен лучший ответ.

Ответ Ивана о модульных тестах, когда вы говорите о тестах свойствтак что давайте не будем обращать на это внимание.

Dfeuer сообщает вам, когда допустимо зеркалировать реализацию, но не о том, что делать для вашего варианта использования.

Это распространенная ошибка в тестах на основе свойств (PBT)сначала переписать код реализации.Но это не то, для чего нужны PBT.Они существуют для проверки свойств вашей функции.Эй, не волнуйтесь, мы все делаем эту ошибку в первые несколько раз, когда мы пишем PBT: D

Тип свойства, который вы можете здесь проверить, является ли ваш ответ функции непротиворечивым с егоinput:

if SUT.shouldDiscountProduct p t 
then isJust (userDiscountCode p) && isJust (productDiscount t) 
else isNothing (userDiscountCode p) || isNothing (productDiscount t)

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

Но вы можете найти и другие типы свойств!Например, неизменность свойства:

SUT.shouldDiscountProduct p{userDiscountCode = Nothing} t == False
SUT.shouldDiscountProduct p{productDiscount = Nothing} t == False

Посмотрите, что мы здесь сделали?Мы зафиксировали одну часть ввода (например, код скидки пользователя всегда пуст) и утверждаем, что независимо от того, как все остальное меняется, вывод является инвариантным (всегда ложным).То же самое касается скидки на продукт.

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

shouldDiscountProduct user product =
  if M.isNothing (userDiscountCode user)
     then False
     else if (productDiscount product) then True
                                       else False

shouldDiscountProduct' user product
  | Just _ <- userDiscountCode user
  , Just _ <- productDiscount product
  = True
  | otherwise = False

SUT.shouldDiscountProduct p t = SUT.shouldDiscountProduct' p t

Что гласит: «Независимо от ввода, переписанная функция всегда должна возвращать то же значение, что и старая функция».Это очень круто при рефакторинге!

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

Обратите внимание, PBT не являются врагом юнит-тестов, они действительно хорошо сочетаются друг с другом.Вы можете использовать 1 или 2 модульных теста, если вы чувствуете себя более уверенно в отношении фактических значений, а затем написать тест (ы) свойств, чтобы утверждать, что ваша функция имеет некоторые поведения, независимо от ввода.

0 голосов
/ 30 октября 2018

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

Чтобы решить эту проблему, тесты построены на следующих принципах:

  • Проверяет подачу предварительно определенных входов и проверку предварительно определенных выходов.Ничего «случайного».Все источники случайности считаются дополнительными входными данными и подвергаются насмешкам или иным образом принуждают производить конкретные значения.
    • Иногда возможен компромисс: вы оставляете случайный источник в покое и проверяете выходные данные не на точное значение, а просто на «правильность» (например, наличие определенного формата).Но тогда вы не тестируете логику, которая отвечает за части, которые вы не проверяете (хотя вам может и не понадобиться, см. Ниже).
  • Единственный способ проверитьфункция полностью состоит в том, чтобы тщательно опробовать все возможные входные данные
  • Поскольку это почти всегда невозможно, выбираются только несколько "представительных"
    • И делается предположение о коде что он обрабатывает все другие возможные входные данные одинаково
      • Вот почему важна метрика покрытия теста: она сообщит вам, когда код изменился таким образом, что это предположение больше не сохраняется

Чтобы выбрать оптимальный «репрезентативный» вход, следуйте интерфейсу функции.

  • Если во входных данных есть некоторые диапазоны, которые срабатываютразличное поведение, значения ребер обычно наиболее полезны
  • Выходы проверяются на соответствие обещаниям интерфейса
    • Иногда интерфейс не обещает определенногозначение для заданных входных данных, вариации считаются деталями реализации.Затем вы проверяете не конкретные значения, а только то, что гарантирует интерфейс.
      • Тестирование деталей реализации полезно только в том случае, если на них полагаются другие компоненты - тогда они на самом деле являются не деталями реализации, а частями отдельного частного интерфейса.
...