Verifying a set of objects have been mapped correctly

纵饮孤独 提交于 2019-12-23 13:24:36

问题


I'm looking for a clean set of ways to manage Test Specific Equality in F# unit tests. 90% of the time, the standard Structural Equality fits the bill and I can leverage it with unquote to express the relation between my result and my expected.

TL;DR "I can't find a clean way to having a custom Equality function for one or two properties in a value which 90% of is well served by Structural Equality, does F# have a way to match an arbitrary record with custom Equality for just one or two of its fields?"


Example of a general technique that works for me

When verifying a function that performs a 1:1 mapping of a datatype to another, I'll often extract matching tuples from both sides of in some cases and compare the input and output sets. For example, I have an operator:-

let (====) x y = (x |> Set.ofSeq) = (y |> Set.ofSeq)

So I can do:

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 @>

This enables me to Assert that each of my inputs has been mapped to an output, without any superfluous outputs.

The problem

The problem is when I want to have a custom comparison for one or two of the fields.

For example, if my DateTime is being passed through a slightly lossy serialization layer by the SUT, I need a test-specific tolerant DateTime comparison. Or maybe I want to do a case-insensitive verification for a string field

Normally, I'd use Mark Seemann's SemanticComparison library's Likeness<Source,Destination> to define a Test Specific equality, but I've run into some roadblocks:

  • tuples: F# hides .ItemX on Tuple so I can't define the property via a .With strongly typed field name Expression<T>
  • record types: TTBOMK these are sealed by F# with no opt-out so SemanticComparison can't proxy them to override Object.Equals

My ideas

All I can think of is to create a generic Resemblance proxy type that I can include in a tuple or record.

Or maybe using pattern matching (Is there a way I can use that to generate an IEqualityComparer and then do a set comparison using that?)

Alternate failing test

I'm also open to using some other function to verify the full mapping (i.e. not abusing F# Set or involving too much third party code. i.e. something to make this pass:

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:


Firstly, thanks to all for the inputs. I was largely unaware of SemanticComparer<'T> and it definitely provides a good set of building blocks for building generalized facilities in this space. Nikos' post gives excellent food for thought in the area too. I shouldn't have been surprised Fil exists too - @ptrelford really does have a lib for everything (the FSharpValue point is also v valuable)!

We've thankfully arrived at a conclusion to this. Unfortunately it's not a single all-encompassing tool or technique, but even better, a set of techniques that can be used as necessary in a given context.

Firstly, the issue of ensuring a mapping is complete is really an orthogonal concern. The question refers to an ==== operator:-

let (====) x y = (x |> Set.ofSeq) = (y |> Set.ofSeq)

This is definitely the best default approach - lean on Structural Equality. One thing to note is that, being reliant on F# persistent sets, it requires your type to support : comparison (as opposed to just : equality).

When doing set comparisons off the proven Structural Equality path, a useful technique is to use HashSet<T> with a custom 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 

the equals parameter of ==|== is 'a -> 'a -> bool which allows one to use pattern matching to destructure args for the purposes of comparison. This works well if either the input or the result side are naturally already tuples. Example:

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 @>

While SemanticComparer<'T> can do a job, it's simply not worth bothering for tuples with when you have the power of pattern matching. e.g. Using SemanticComparer<'T>, the above test can be expressed as:

test <@ expecteds ==~== results 
    <| [ funNamedMemberComparer "Item2" equalsWithinASecond ] @>

using the helper:

[<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

All of the above is best understood by reading Nikos Baxevanis' post NOW!

For types or records, the ==|== technique can work (except critically you lose Likeness<'T>s verifying coverage of fields). However the succinctness can make it a valuable tool for certain sorts of tests :-

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


来源:https://stackoverflow.com/questions/20284177/verifying-a-set-of-objects-have-been-mapped-correctly

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!