一套快速检查测试与实现相匹配是好还是坏?

dan*_*oks 7 haskell functional-programming business-logic quickcheck property-based-testing

我正在尝试使用Haskell的QuickCheck,虽然我熟悉测试方法背后的概念,但这是我第一次尝试将它用于一个超出测试类似的项目的那个项目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
Run Code Online (Sandbox Code Playgroud)

对于此功能,我可以编写如下的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
Run Code Online (Sandbox Code Playgroud)

我最终得到的是一个功能,可以更优雅地expectation验证当前的实现shouldDiscountProduct.所以现在我有一个测试,我可以重构我原来的功能.但我的自然倾向是将其改为expectation:

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

但这很好吗?如果我想在将来再次更改此功能,我可以使用相同的功能来验证我的更改是否合适并且不会无意中破坏某些内容.

或者是这种过度/双重记账?我想我从OOP测试中已经根深蒂固,你应该尽量避免镜像实现细节,这实际上不可能是这个,它是实现!

然后我想,当我浏览我的项目并添加这些类型的测试时,我实际上将添加这些测试,然后重构到我在expectation断言中实现的更清晰的实现.显然,对于比这些更复杂的功能,情况并非如此,但在这一轮中,我认为情况就是如此.

人们使用基于属性的测试来处理业务逻辑类型的功能有什么经验?对于这种事情,有什么好的资源吗?我想我只是想验证我是否以适当的方式使用QC,而我的OOP过去在我脑海中对此产生怀疑......

Sir*_*r0n 2

很抱歉几个月后才介入,但由于这个问题很容易在谷歌上出现,我认为它需要一个更好的答案。

当您谈论属性测试时,伊万的回答是关于单元测试的,所以让我们忽略它。

Dfeuer 告诉您何时可以接受镜像实现,但不会告诉您针对您的用例做什么。

首先重写实现代码是基于属性的测试 (PBT) 的一个常见错误。但这不是 PBT 的目的。它们的存在是为了检查函数的属性。嘿,别担心,我们在最初几次写 PBT 时都会犯这个错误:D

您可以在此处检查的一种属性是您的函数响应是否与其输入一致:

if SUT.shouldDiscountProduct p t 
then isJust (userDiscountCode p) && isJust (productDiscount t) 
else isNothing (userDiscountCode p) || isNothing (productDiscount t)
Run Code Online (Sandbox Code Playgroud)

这在您的特定用例中很微妙,但请注意,我们颠倒了逻辑。您的测试检查输入,并基于此对输出进行断言。我的测试检查输出,并基于此对输入进行断言。在其他用例中,这可能不太对称。大多数代码也可以重构,我让你做这个练习;)

但您可能会发现其他类型的房产!例如不变性

SUT.shouldDiscountProduct p{userDiscountCode = Nothing} t == False
SUT.shouldDiscountProduct p{productDiscount = Nothing} t == False
Run Code Online (Sandbox Code Playgroud)

看看我们在这里做了什么?我们固定了输入的一部分(例如,用户折扣代码始终为空),并且我们断言无论其他一切如何变化,输出都是不变的(始终为假)。产品折扣也是如此。

最后一个例子:您可以使用类似的属性来检查旧代码,而新代码的行为完全相同:

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
Run Code Online (Sandbox Code Playgroud)

其读作是“无论输入如何,重写的函数必须始终返回与旧函数相同的值”。重构时这太酷了!

我希望这可以帮助您掌握基于属性的测试背后的想法:停止过多担心函数返回的值,并开始想知道函数的某些行为。

请注意,PBT 并不是单元测试的敌人,它们实际上非常适合在一起。如果您对实际值感到更安全,您可以使用 1 或 2 个单元测试,然后编写属性测试来断言您的函数具有某些行为,无论输入如何。