Haskell有一个很好的属性,一段代码的类型签名有时会告诉你代码的作用。这让我思考......你能根据类型签名构建测试吗?
例如,考虑一个诸如Data.Map
之类的模块。它定义了一种数据类型和几种在该类型上运行的函数。仅查看类型签名,[原则上]应该可以找出构造Map
值的所有可能方法。我的意思是算法可能 - 它应该可以编写一些程序,将接口连接到你的模块,并找出你可以对它进行的所有可能的函数调用。
现在,有些人认为编写良好的测试套件可以被视为图书馆应该做的“规范”。显然,编写代码的人知道他们想要做什么,而机器却不知道。但是考虑到你提供的类型签名,机器应该能够计算出可能要求的内容。
作为一个更具体的情况,假设有一组不变量应该应用于所有可能的Map
值。 (例如,null m == (size m == 0)
,或者内部完整性检查函数应该总是返回true。)可以想象编写一个测试框架,将模块交给模块,然后说“类型Map X Y
的值应该总是满足这些不变量“,让测试框架消失并尝试执行您提供的函数的每个可能组合来生成映射并检查它们是否满足所有条件。如果没有,它会告诉您违反了什么条件以及构造此无效值所需的表达式。
我的问题是,这种方法听起来容易处理吗?可取?有趣?你会如何解决这样的问题? Djinn似乎已经知道如何在给定预先存在的函数和常量的情况下构造指定类型的值,因此尝试提出具有给定类型的多个表达式听起来并不太难。尝试获得良好的代码覆盖率可能适用何种启发式方法? (显然,分析代码而不仅仅是类型是非常困难的...赖斯的定理潜伏在等待中。)
(请注意,我并不是说这种机器检查应该替换手写的测试代码。这个想法更多的是扩充它;也许更难例如,机器可以咳出可能的表达式,然后向人类询问“正确”答案应该是什么。然后可以将其记录为新的测试用例,以便在测试套件运行时运行。)
答案 0 :(得分:3)
你现在可以使用的最近的东西 是QuickCheck或SmallCheck ---“基于属性的”测试库。
类似QuickCheck的测试的典型示例是reverse
>>> quickCheck $ \s -> s == reverse (reverse s)
True
QuickCheck使用类型类机制创建各种类型的随机(称为Arbitrary
)示例,然后检查这些随机实例的属性。正确选择的Arbitrary
Map
实例可以为创建您正在寻找的各种测试提供一些方法。
这里有更多批次选项。如果您检查一些依赖类型的语言,如Idris和Agda,您可以在一个功能更强大的类型系统上获得“受Haskell影响”的语法,该系统能够静态地证明程序的属性。这远远高于QuickCheck(它已经远远高于单元测试),因为它不再依赖于创建合适的Arbitrary
实例的能力,这些实例适当地捕获感兴趣的问题空间。
除了完全依赖类型的语言,您可能会对使用Liquid Haskell的某些类型证明感兴趣。
答案 1 :(得分:1)
扩展“aavogt”的评论:
Irulan获取一组标识符,构造涉及这些标识符的所有可能良好类型的表达式,并搜索在执行时抛出异常的表达式。
如果函数抛出异常,当然不一定是 bug ;可能存在你期望抛出异常的表达式。但是Irulan发现了所有这些并向你展示了它们,所以你可以决定哪些是合法的,哪些是错误的。
看来Irulan不会发现任何不会导致异常的错误。 (特别是应该抛出异常但不会被发现的表达式。)不会捕获非终止或不正确的结果,也不会过多地使用资源。 (那么,机器怎么会自动弄清楚“正确”结果应该是什么?心灵感应?)
我发现令人着迷的是测试数据生成的方法。 Irulan查找类型的值构造函数,或者如果它们不在范围内,则尝试查找生成适当类型值的函数。它根本不使用Eq
个实例,更喜欢使用大小写块(如果无法获取构造函数的情况下使用投影函数)来检查值。
Irulan如何使用懒惰的技巧来懒惰生成测试数据。例如,如果您正在尝试测试length
函数,那么数据实际上在列表中是什么并不重要,只有有多大清单是。 Irulan可以自动计算出来,因此它会生成几个不同大小的列表,但它不会将任何数据放入列表中。像QuickCheck这样的框架将无用地生成数百个具有相同列表大小但内容不同的测试用例。
例如,Irulan可以生成一个包含三个“洞”的列表。如果您触摸其中一个洞,则会引发异常。但是,孔是编号的,例外中有孔号。 Irulan捕获异常,因此“知道”你触摸的是哪个洞。然后它可以系统地用那些可能存在的良好类型的值替换该孔(本身递归地再次填充孔)。在这种方法中,搜索树仅被修剪为对实际测试的代码有用的内容。
我没有意识到异常和懒惰以这种方式相互作用,以允许人们“观察”否则是不透明代码的内部操作。我发现真的很有趣......
我还应该指出,Irulan博士论文包含了一个非常详尽的调查,其中涉及这个问题的其他工作。
答案 2 :(得分:0)
扩展 Thomas DuBuisson 的评论:
为QuickSpec提供一组值(通常是函数值),并且[概念上]使用这些值构造所有可能良好类型的表达式,并尝试查找始终保持的等式。 (值得注意的是,这需要Eq
的正确的实现。)
例如,您可以从empty
给出类似insert
,delete
和Data.Map
的内容,并期望获得insert x (insert y empty) == insert y (insert x empty)
等规则向前。同样,只有当Data.Map
具有Eq
实例并且它实际上正常工作时,这才能正常工作。
这个想法似乎是,如果你看到弹出的规则显然是假的,或者如果你没有看到你期望看到的规则,那么你的代码中可能存在一个错误。 (然后,如果您知道您期望的规则,请首先使用QuickCheck!)另一种可能性是让QuickSpec生成规则系统,然后将这些规则转换为QuickCheck属性。现在,您可以重构代码中的地狱,并检查其可观察行为是否会发生变化。
这是一个有趣的方法来接近这个主题,如果有点奇怪的话。顺便说一句,我发现QuickSpec实际上可以凭空捏造这样的规则系统,这令人着迷。看起来几乎是神奇的。很酷的东西......