4

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 @>
Community
  • 1
  • 1
Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
  • 1
    Have you looked at SemanticComparison's [`SemanticComparer`](https://github.com/AutoFixture/AutoFixture/blob/master/Src/SemanticComparison/SemanticComparer.cs#L175)? I don't know if it can help you with these issues, but it's the best suggestion I can currently make. – Mark Seemann Nov 29 '13 at 14:14
  • @MarkSeemann Thanks; will have a peep. At first glance it looks like it might surmount the `propertyPicker` and Record Types being `sealed` issues. (Had ruled it out a bit too due to a lot of the `Likeness` facilities being superfluous in this context. To be honest I've been poring over the discussion of [#99](https://github.com/AutoFixture/AutoFixture/issues/99) more than once and simply not getting it (admittedly I didnt walk the code) -- until now!). – Ruben Bartelink Nov 29 '13 at 15:14
  • 1
    @RubenBartelink not sure if it'll work for you in this context but you can reflect over records & tuples using the [Reflection.FSharpValue class](http://msdn.microsoft.com/en-us/library/ee353505.aspx) & as a last resort [Fil](https://fil.codeplex.com/) lets you generate F# compatible record types on the fly – Phillip Trelford Nov 29 '13 at 18:36
  • 1
    @RubenBartelink As Mark Seemann wrote, [`SemanticComparer`](https://github.com/AutoFixture/AutoFixture/blob/master/Src/SemanticComparison/SemanticComparer.cs#L175) could possibly help. FWIW, it [works with F# structural types](http://nikosbaxevanis.com/blog/2013/12/07/semantic-equality-comparison-in-f-number/) also. [The current API is a work in progress](https://github.com/AutoFixture/AutoFixture/issues/219). – Nikos Baxevanis Dec 07 '13 at 07:56

1 Answers1

2

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
Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
  • @NikosBaxevanis Wouldn't have been possible without your blog post. I'm very much looking forward to seeing how clean a DSL you can come up with ! :P (I'm slightly concerned about losing the Member-name comparison that a `Likeness` will give though). some more Albedo trickery will no doubt have a part to play too! This problem space is definitely ripe for exploration! I think there's also scope for using Active Patterns in the mix too. Also a lot of the credit is due to [my colleague Adam](https://github.com/adamjasinski) ! – Ruben Bartelink Dec 10 '13 at 17:46