f# – 已正确映射验证一组对象

我正在寻找一套简洁的方法来管理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
点赞