我如何测试某些东西对地图中的所有元素都有效?

How do I test that something is valid for all elements in a map?

我的申请中有以下内容:

newtype User = User Text
newtype Counts = Counts (Map User Int)

subjectUnderTest :: Counts -> Text

正确输出的示例是

> subjectUnderTest $ fromList [(User "foo", 4), (User "bar", 4), (User "qux", 2)]
"4: foo, bar\n2: qux"

我想编写基于 属性 的测试来验证诸如“所有用户都在输出中表示”、“所有计数都在输出中表示”和“所有用户都在同一行上”之类的东西作为他们相应的计数”。这些属性的共同点是它们的措辞以“all ...”开头。

如何编写一个 属性 来验证某些内容对 Map 中的每个元素都有效?

我假设这个问题只是更复杂问题的简化表示,所以这里有一些策略需要考虑:

拆分功能

看起来 subjectUnderTest 做了两件不相关的事情:

  1. 它按值而不是键对映射中的值进行分组。
  2. 它格式化,或 pretty-prints,倒置地图。

如果您可以将功能拆分为这两个步骤,则它们更容易单独测试。

第一步,可以进行参数化多态。与其测试类型为 Counts -> Text 的函数,不如考虑测试类型为 Eq b => Map a b -> [(b, [a])] 的函数。 Property-based 使用参数多态性测试更容易,因为 you get certain properties for free。例如,您可以确定输出中的值只能来自输入,因为无法凭空变出 ab 值。

您仍然需要为您询问的属性编写测试。编写一个类型为 Eq b => Map a b -> Testable 的函数。如果您想测试所有值是否都存在,请将它们从地图中拉出并列出它们。对列表进行排序并 nub 它。它现在是 [b] 值。这是您的预期输出。

现在调用你的函数。它 returns 类似于 [(b, [a])]。使用 fst 对其进行映射,对其进行排序并 nub。该列表应该等于您的预期输出。

下一步 (pretty-printing),请参阅下一节。

往返

当你想要property-basepretty-printing时,最简单的方法通常是硬着头皮写一个解析器。打印机和解析器应该是彼此的对偶,所以如果你有一个函数 MyType -> String,你应该有一个类型为 String -> Maybe MyType.

的解析器

您现在可以像 MyType -> Testable 这样写一个通用的 属性。它以 MyType 的值作为输入(我们称它为 expected)。您现在生成一个值(我们称之为 actual)作为 actual = parse $ print expected。您现在可以验证 Just expected === actual.

如果特定的 String 格式很重要,我会使用 good old parametrised tests.

用几个实际的例子来跟进它

仅仅因为您正在进行 property-based 测试并不意味着 'normal' 单元测试也没有用。


例子

这是我上面的意思的一个简单例子。假设

invertMap :: (Ord b, Eq b) => Map a b -> [(b, [a])]

您可以将属性之一定义为:

allValuesAreNowKeys :: (Show a, Ord a) => Map k a -> Property
allValuesAreNowKeys m =
  let expected = nub $ sort $ Map.elems m
      actual = invertMap m
  in expected === nub (sort $ fmap fst actual)

由于这个 属性 仍然是参数多态的,您必须将它添加到具有特定类型的测试套件中,例如:

tests = [
  testGroup "Sorting Group 1" [
    testProperty "all values are now keys" (allValuesAreNowKeys :: Map String Int -> Property)]]

有更漂亮的方法来定义属性列表;那个只是 quickcheck-test-framework 堆栈模板使用的模板...