我正在寻找一套简单的方法来管理 F#单元测试中的Test Specific Equality。 90%的时间,standard Structural Equality符合条款,我可以将其与unquote一起用来表达我的result
和我的expected
之间的关系。
TL; DR“我找不到一种干净的方法,可以为一个或两个属性设置一个自定义Equality函数,其中90%的结果是由Structural Equality提供的,F#是否有办法匹配任意记录只有一个或两个字段的自定义平等?“
在验证执行数据类型到另一个数据类型的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 @>
这使我Assert
能够将每个输入映射到输出,而没有任何多余的输出。
问题是当我想要对一个或两个字段进行自定义比较时。
例如,如果SUT通过稍微有损的序列化层传递我的DateTime,我需要一个特定于测试的容忍DateTime
比较。或者我想对string
字段进行不区分大小写的验证
通常情况下,我会使用Mark Seemann的SemanticComparison库的Likeness<Source,Destination>
来定义测试特定的等式,但我遇到了一些障碍:
.ItemX
隐藏Tuple
,因此我无法通过.With
强类型字段名Expression<T>
sealed
,没有选择退出,因此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 @>
答案 0 :(得分:2)
首先,感谢大家的投入。我基本上没有意识到SemanticComparer<'T>
,它确实提供了一套很好的构建模块,用于在这个空间中建造通用设施。 Nikos' post也为该地区提供了极好的思考。我不应该感到惊讶Fil也存在 - @ptrelford确实有一个lib的所有内容(FSharpValue
点也很有价值)!
我们很高兴得出结论。不幸的是,它不是一个包罗万象的工具或技术,但更好的是,在给定的环境中可以根据需要使用一组技术。
首先,确保映射完成的问题实际上是一个正交问题。问题涉及====
运算符: -
let (====) x y = (x |> Set.ofSeq) = (y |> Set.ofSeq)
这绝对是最好的默认方法 - 依靠结构平等。需要注意的一点是,依赖于F#持久集,它需要您的类型支持: comparison
(而不仅仅是: equality
)。
在对经过验证的结构平等路径进行集合比较时,一种有用的技巧是将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
equals
的{{1}}参数为==|==
,允许人们使用模式匹配来构造args以进行比较。如果输入或结果方面自然已经是元组,这很有效。例如:
'a -> 'a -> bool
虽然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 ] @>
现在阅读Nikos Baxevanis' post现在可以最好地理解上述所有内容。
对于类型或记录,[<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
技术可以正常工作(除了关键,您将失去==|==
验证字段的覆盖范围)。然而,简洁可以使它成为某些测试的宝贵工具: -
Likeness<'T>