验证一组对象已正确映射

时间:2013-11-29 11:00:07

标签: f# autofixture

我正在寻找一套简单的方法来管理 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>来定义测试特定的等式,但我遇到了一些障碍:

  • 元组:F#在.ItemX隐藏Tuple,因此我无法通过.With强类型字段名Expression<T>
  • 定义该属性
  • 记录类型:TTBOMK这些是F#的sealed,没有选择退出,因此SemanticComparison无法代理它们来覆盖Object.Equals

我的想法

我能想到的是创建一个通用的Resemblance proxy type,我可以将其包含在元组或记录中。

或者可能使用模式匹配(有没有办法可以用它来生成IEqualityComparer,然后使用它进行集合比较?)

替代失败测试

我也愿意使用其他一些功能来验证完整的映射(即不滥用F#Setinvolving 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 @>

1 个答案:

答案 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>