我正在寻找一套简洁的方法来管理F#单元测试中的
Test Specific Equality. 90%的时间,
standard Structural Equality符合要求,我可以利用
unquote来表达我的结果与我的期望之间的关系.
TL; DR“我找不到一种简洁的方法来为一个或两个属性提供一个自定义Equality函数,其中90%的结果是由Structural Equality提供的,F#是否有办法将任意记录与自定义Equality相匹配只有一两个领域?“
适用于我的一般技术示例
在验证执行数据类型到另一个数据类型的1:1映射的函数时,我通常会在某些情况下从两端提取匹配的元组,并比较输入和输出集.例如,我有一个操作符: –
let (====) x y = (x |> Set.ofSeq) = (y |> Set.ofSeq)
所以我可以这样做:
let inputs = ["KeyA",DateTime.Today; "KeyB",DateTime.Today.AddDays(1); "KeyC",DateTime.Today.AddDays(2)]
let trivialFun (a:string,b) = a.ToLower(),b
let expected = inputs |> Seq.map trivialFun
let result = inputs |> MyMagicMapper
test <@ expected ==== actual @>
这使我能够断言我的每个输入都已映射到输出,没有任何多余的输出.
问题
问题是当我想要对一个或两个字段进行自定义比较时.
例如,如果SUT通过稍微有损的序列化层传递我的DateTime,我需要一个特定于测试的容忍DateTime比较.或者我想对字符串字段进行不区分大小写的验证
通常情况下,我会使用Mark Seemann的SemanticComparison库的相似度< Source,Destination>定义测试特定的相等,但我遇到了一些障碍:
>元组:F#隐藏了元组上的.ItemX,因此我无法通过强类型字段名称表达< T>来定义属性.
>记录类型:TTBOMK这些由F#密封,没有选择退出,因此SemanticComparison不能代理它们来覆盖Object.Equals
我的想法
我能想到的是创建一个通用的Resemblance proxy type,我可以将其包含在元组或记录中.
或者也许使用模式匹配(有没有一种方法可以用它来生成IEqualityComparer,然后使用它进行集合比较?)
替代测试失败
我也愿意使用其他一些函数来验证完整的映射(即不滥用F#Set或involving too much third party code.即要使这个传递的东西:
let sut (a:string,b:DateTime) = a.ToLower(),b + TimeSpan.FromTicks(1L)
let inputs = ["KeyA",DateTime.Today; "KeyB",DateTime.Today.AddDays(1.0); "KeyC",DateTime.Today.AddDays(2.0)]
let toResemblance (a,b) = TODO generate Resemblance which will case insensitively compare fst and tolerantly compare snd
let expected = inputs |> List.map toResemblance
let result = inputs |> List.map sut
test <@ expected = result @>
最佳答案 首先,感谢所有人的投入.我基本上没有意识到
SemanticComparer<'T>
,它确实提供了一套很好的构建模块,用于在这个空间内建造通用设施.
Nikos’ post也为该地区提供了极好的思考.我不应该感到惊讶
Fil也存在 – @ptrelford确实有一个lib的一切(
FSharpValue
点也是有价值的)!
我们很幸运地得出了结论.不幸的是,它不是一个包罗万象的工具或技术,而是更好的一组技术,可以在给定的环境中根据需要使用.
首先,确保映射完成的问题实际上是一个正交问题.问题是指====运算符: –
let (====) x y = (x |> Set.ofSeq) = (y |> Set.ofSeq)
这绝对是最好的默认方法 – 依靠结构平等.需要注意的一点是,依赖于F#持久集,它需要您的类型支持:比较(而不仅仅是:相等).
当从已证明的结构平等路径进行集合比较时,一种有用的技术是使用HashSet< T>.使用自定义IEqualityComparer: –
[<AutoOpen>]
module UnorderedSeqComparisons =
let seqSetEquals ec x y =
HashSet<_>( x, ec).SetEquals( y)
let (==|==) x y equals =
let funEqualityComparer = {
new IEqualityComparer<_> with
member this.GetHashCode(obj) = 0
member this.Equals(x,y) =
equals x y }
seqSetEquals funEqualityComparer x y
== | ==的等于参数是’a – > ‘a – > bool允许人们使用模式匹配来构造args以进行比较.如果输入或结果方面自然已经是元组,这很有效.例:
sut.Store( inputs)
let results = sut.Read()
let expecteds = seq { for x in inputs -> x.Name,x.ValidUntil }
test <@ expecteds ==|== results
<| fun (xN,xD) (yN,yD) ->
xF=yF
&& xD |> equalsWithinASecond <| yD @>
而SemanticComparer<‘T>当你拥有模式匹配的力量时,它根本不值得为元组打扰.例如使用SemanticComparer<‘T>,上述测试可表示为:
test <@ expecteds ==~== results
<| [ funNamedMemberComparer "Item2" equalsWithinASecond ] @>
使用帮手:
[<AutoOpen>]
module MemberComparerHelpers =
let funNamedMemberComparer<'T> name equals = {
new IMemberComparer with
member this.IsSatisfiedBy(request: PropertyInfo) =
request.PropertyType = typedefof<'T>
&& request.Name = name
member this.IsSatisfiedBy(request: FieldInfo) =
request.FieldType = typedefof<'T>
&& request.Name = name
member this.GetHashCode(obj) = 0
member this.Equals(x, y) =
equals (x :?> 'T) (y :?> 'T) }
let valueObjectMemberComparer() = {
new IMemberComparer with
member this.IsSatisfiedBy(request: PropertyInfo) = true
member this.IsSatisfiedBy(request: FieldInfo) = true
member this.GetHashCode(obj) = hash obj
member this.Equals(x, y) =
x.Equals( y) }
let (==~==) x y mcs =
let ec = SemanticComparer<'T>( seq {
yield valueObjectMemberComparer()
yield! mcs } )
seqSetEquals ec x y
现在阅读Nikos Baxevanis’ post可以最好地理解以上所有内容!
对于类型或记录,== | ==技术可以工作(除了批判性地你失去了相似性<‘T> s验证字段的覆盖范围).然而,简洁可以使它成为某些测试的有价值的工具: –
sut.Save( inputs)
let expected = inputs |> Seq.map (fun x -> Mapped( base + x.ttl, x.Name))
let likeExpected x = expected ==|== x <| (fun x y -> x.Name = y.Name && x.ValidUntil = y.ValidUntil)
verify <@ repo.Store( is( likeExpected)) @> once