如何使用基于 属性 的测试库为 reducer 生成一组有效操作?

How to generate a set of valid actions for a reducer using a property based testing library?

我正在尝试生成一系列操作,其中应生成的操作取决于之前的操作。

假设我的状态是一组存储为数组的数字:

[1, 2]

并且有以下动作:

{ type: "add", value: number }
{ type: "remove", value: number }

我想在检查状态属性之前生成一系列要分派的操作。如果生成删除操作,我想确保它的值处于状态。

有效示例:

initial state: [1, 2]
[{ type: "remove", value: 1 }, { type: "remove", value: 2 }]
[{ type: "add", value: 3 }, { type: "remove", value: 3 }]

无效示例:

initial state: [1, 2]
[{ type: "remove", value: 3 }]
[{ type: "remove", value: 2 }, { type: "remove", value: 2 }]

这是否可以使用基于 属性 的测试库实现?如果可以,我将如何实现?

我正在使用 https://github.com/dubzzz/fast-check,但如果使用其他库更容易,我愿意接受其他人的示例。

是的,这非常适合基于 属性 的测试。通过快速浏览 fast-check 的文档,我可以看到三种方法:

  • 做一个precondition。这将忽略生成无效操作序列的测试。
    在 运行 删除操作之前,您会计算该数字包含在初始状态中的频率、之前在添加操作中的频率以及之前在删除操作中的频率。到时候你就知道能不能去掉了。
  • 使用model-based testing (also here)。这似乎非常适合您的用例。每个动作将由一个 Command 表示,所有命令将简单地应用它们各自的动作,并且在它们的 check 方法中,您将验证该动作是否符合条件。
    这需要建立一个模型,您需要确保该模型是对实际状态的简化,并且它使用不同的实现方法(这样您就不会在这里重新实现您的错误)。在您的示例中,这可能意味着保留 Set 个出现的数字或 Map 个它们的计数,而不是有序数组。
  • 首先只生成有效序列。
    这比前两种方法更有效,但通常也更复杂。但是,如果要删除的生成数字过于公正并且很少与列表中的数字匹配,则可能有必要。我在这里有两个想法:
    • 以递归方式生成您的操作列表,并保留与基于模型的测试中的模型相似的模型。但是,您必须自己更新它。有了这个,您可以仅为模型中当前存在的那些数字生成删除操作。
      我不确定是否 letrec or memo help here, whether you might need to use chain,或者要求库作者为这个用例提供额外的功能。 (甚至可能作为基于模型的测试的一部分,其中 Command 个实例可以从当前模型动态派生?)
    • 生成一个删除操作,并始终与前面具有相同编号的添加操作一起生成。生成 [add(x)][add(y), remove(y)] 列表后, 合并 这些列表以任意顺序排列,但保持每个子列表元素之间的相应顺序。
      这可能是执行此操作的最优雅的方法,因为它看起来与您所在州的模型完全不同。但是,我很确定您将需要 build your own Arbitrary 来实现 randomMerge 功能 - 也许向库作者寻求帮助或请求新功能。