领域层测试:避免副作用与陷阱

在软件开发过程中,测试是一个至关重要的环节。它帮助确保代码的质量和稳定性。然而,当谈论纯领域逻辑的测试时,往往不会那么挑剔。单元测试中存在许多陷阱,比如过度指定的软件。即使在测试看似简单的纯函数时,也可能遇到一些坑。

一个常见的问题是,在编写单元测试时,依赖于一些任意的“魔法数字”。虽然可以保证函数在给定点上工作正确,但不能保证它在每个点上都工作。一个替代方案是检查函数是否满足某些连续的标准。

这就是属性测试的目的。它不是在硬编码的输入点上验证输出,而是用大量生成的值来检查定义的函数的属性。

让通过代码示例来看看这是如何工作的。下面是一个来自项目“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函数,后者可能会发生变化。有了这样的设置,就可以检查被测试的函数是否满足提供的属性,并且有大量由库生成的随机值。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485