在软件开发过程中,测试是一个至关重要的环节。它帮助确保代码的质量和稳定性。然而,当谈论纯领域逻辑的测试时,往往不会那么挑剔。单元测试中存在许多陷阱,比如过度指定的软件。即使在测试看似简单的纯函数时,也可能遇到一些坑。
一个常见的问题是,在编写单元测试时,依赖于一些任意的“魔法数字”。虽然可以保证函数在给定点上工作正确,但不能保证它在每个点上都工作。一个替代方案是检查函数是否满足某些连续的标准。
这就是属性测试的目的。它不是在硬编码的输入点上验证输出,而是用大量生成的值来检查定义的函数的属性。
让通过代码示例来看看这是如何工作的。下面是一个来自项目“Kyiv Station Walk”的示例。可以看到这个函数接受一个领域中的检查点集合,并将其转换,使其符合表示层的规则。
let removeRedundantCheckpoints (checkPoints : Location[]) =
let checkPointsMaxCount = 5
let isStartOrEndOfTheRoute (checkPoints : Location[]) i =
i = 0 || i = checkPoints.Length - 1
let euclidianDistance c1 c2 =
Math.Pow(float(c1.lattitude - c2.lattitude), 2.0) +
Math.Pow(float(c1.longitude - c2.longitude), 2.0)
if checkPoints.Length <= 5 then
checkPoints
else
checkPoints
|> Array.mapi (fun i c ->
if isStartOrEndOfTheRoute checkPoints i then
{ index = i; checkPoint = c; distanceToNextCheckPoint = 1000000.0 }
else
{ index = i; checkPoint = c; distanceToNextCheckPoint = euclidianDistance checkPoints.[i+1] c }
)
|> Array.sortByDescending (fun i -> i.distanceToNextCheckPoint)
|> Array.take(checkPointsMaxCount)
|> Array.sortBy (fun i -> i.index)
|> Array.map (fun i -> i.checkPoint)
可以提供一些任意的检查点数组来检查输出,或者,可以思考一下函数应该满足的一些属性。以下是这些属性在代码中的表达。
open FsCheck.Xunit
open RouteModels
module RemoveRedundantCheckpointsTests =
let ``result array contains no more than 5 items`` input mapFn =
let res = mapFn input
Array.length res <= 5
[]
let maxLength x =
``result array contains no more than 5 items`` x removeRedundantCheckpoints
let ``result contains first point from input`` (input: Location[]) (mapFn : Location[] -> Location[]) =
if Array.length input = 0 then true else
let res = mapFn input
res.[0] = input.[0]
[]
let firstItem x =
``result contains first point from input`` x removeRedundantCheckpoints
let ``result contains last point from input`` (input: Location[]) (mapFn : Location[] -> Location[]) =
if Array.length input = 0 then true else
let res = mapFn input
res.[res.Length-1] = input.[input.Length-1]
[]
let lastItem x =
``result contains last point from input`` x removeRedundantCheckpoints
let ``result contains only points from input`` input mapFn =
let res = mapFn input
Array.length (Array.except input res) = 0
[]
let onlyInput x =
``result contains only points from input`` x removeRedundantCheckpoints
正如从导入语句中看到的,依赖于FsCheck来为生成一些随机值。在代码的后面,声明了一个高阶函数,它接受映射函数和输入数组,并返回一个布尔条件,检查属性是否满足。双反引号是F#的一个方便特性,它允许用自然语言表达属性。
测试用Property属性装饰,并接受FsCheck生成的输入以及removeRedundantCheckpoints函数,后者可能会发生变化。有了这样的设置,就可以检查被测试的函数是否满足提供的属性,并且有大量由库生成的随机值。