我正在寻找一套干净的方法来管理中的Test Specific Equality F#单元测试。 90%的时间standard Structural Equality符合要求,我可以将其与unquote结合使用以表达result
和expected
之间的关系。
TL; DR“我找不到一种干净的方法来为一个或两个属性提供一个自定义的Equality函数,该值的90%都可以很好地由Structured Equality服务,F#是否有一种方法可以将任意记录与自定义的Equal相匹配仅用于其一两个 Realm ?”
对我有用的通用技术示例
验证执行将数据类型与其他数据类型进行1:1映射的函数时,在某些情况下,我经常会从两边提取匹配的元组,并比较输入和输出集。例如,我有一个运算符(operator):
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 @>
最佳答案
首先,感谢大家的投入。我基本上不知道 SemanticComparer<'T>
,它肯定为在该空间中建立通用设施提供了很好的构建块。 Nikos' post也为该地区提供了令人深思的美食。我不应该感到惊讶Fil也存在-@ptrelford确实确实为所有内容提供了一个库( FSharpValue
点也很有值(value))!
幸运的是,我们已经得出了结论。不幸的是,这不是一个单一的,包罗万象的工具或技术,甚至更好的是,可以在给定上下文中根据需要使用的一组技术。
首先,确保映射完整的问题实际上是一个正交的问题。问题涉及到====
运算符:
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
的==|==
参数是'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,可以最好地理解以上所有内容!
对于类型或记录,
==|==
技术可以起作用(除非严重,否则您会丢失Likeness<'T>
,从而验证字段的覆盖范围)。但是,简洁可以使其成为某些测试的有值(value)的工具: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