我正在寻找一套干净的方法来管理中的Test Specific Equality F#单元测试。 90%的时间standard Structural Equality符合要求,我可以将其与unquote结合使用以表达resultexpected之间的关系。

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>定义“特定于测试”的相等性,但是遇到了一些障碍:
  • 元组: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 @>
    

    最佳答案

    首先,感谢大家的投入。我基本上不知道 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
    

    08-18 22:29