Проверка правильности сопоставления набора объектов

Я ищу чистый набор способов управления Конкретное равенство тестов в модульных тестах F#. В 90% случаев стандартное структурное равенство отвечает всем требованиям, и я могу использовать его с снять кавычки, чтобы выразить связь между моими result и моими expected.

TL; DR «Я не могу найти чистый способ иметь пользовательскую функцию равенства для одного или двух свойств в значении, 90% которого хорошо обслуживается структурным равенством, есть ли в 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 сопоставлять каждый из моих входов с выходом без каких-либо лишних выходов.

Эта проблема

Проблема в том, что я хочу иметь собственное сравнение для одного или двух полей.

Например, если мой DateTime проходит через уровень сериализации с небольшими потерями с помощью SUT, мне нужно толерантное DateTime сравнение для конкретного теста. Или, может быть, я хочу сделать проверку без учета регистра для поля string

Обычно я использую библиотеку Likeness<Source,Destination> SemanticComparison Марка Симанна для определения Проверьте конкретное равенство, но я столкнулся с некоторыми препятствиями:

  • кортежи: F# скрывает .ItemX в Tuple, поэтому я не могу определить свойство через .With строго типизированное имя поля Expression<T>
  • типы записей: TTBOMK это sealed от F# без отказа, поэтому SemanticComparison не может проксировать их, чтобы переопределить Object.Equals

Мои идеи

Все, что я могу придумать, это создать общий прокси типа сходства, который я может включаться в кортеж или запись.

Или, может быть, используя сопоставление с образцом (можно ли использовать это для создания IEqualityComparer, а затем выполнить сравнение набора, используя это?)

Альтернативный провальный тест

Я также готов использовать некоторые другие функции для проверки полного сопоставления (т.е. не злоупотреблять F# Set или включает слишком много стороннего кода, т.е. что-то, что нужно сделать:

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

person Ruben Bartelink    schedule 29.11.2013    source источник
comment
Вы смотрели SemanticComparer<T> от SemanticComparison? Я не знаю, может ли это помочь вам с этими проблемами, но это лучшее предложение, которое я могу сейчас сделать.   -  person Mark Seemann    schedule 29.11.2013
comment
@MarkSeemann Спасибо; будет заглянуть. На первый взгляд кажется, что это может преодолеть проблемы propertyPicker и типов записей, являющиеся sealed. (Я тоже немного исключил это из-за того, что многие Likeness средства были излишними в этом контексте. Честно говоря, я корпел над обсуждением #99 более одного раза и просто не получил его (правда, я не прошел код) - до сих пор!).   -  person Ruben Bartelink    schedule 29.11.2013
comment
@RubenBartelink не уверен, что это сработает для вас в этом контексте, но вы можете поразмышлять над записями и кортежами, используя класс Reflection.FSharpValue и, в крайнем случае, File позволяет создавать Типы записей, совместимые с F#, на лету   -  person Phillip Trelford    schedule 29.11.2013
comment
@RubenBartelink Как писал Марк Симанн, SemanticComparer<T> возможно может помочь. Между прочим, он работает со структурными типами F#< /а> также. Текущий API находится в стадии разработки.   -  person Nikos Baxevanis    schedule 07.12.2013


Ответы (1)


Во-первых, спасибо всем за участие. Я почти ничего не знал о SemanticComparer<'T>, и это определенно обеспечивает хороший набор строительных блоков для создания общих объектов в этом пространстве. пост Никоса дает отличную еду для мысли в области тоже. Меня не должно было удивить, что File тоже существует — у @ptrelford действительно есть библиотека для всего (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 для ==|== равен '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 ] @>

с помощью помощника:

[<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 Пост Баксеваниса СЕЙЧАС!

Для типов или записей может работать метод ==|== (за исключением того, что критически вы теряете Likeness<'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
person Ruben Bartelink    schedule 10.12.2013
comment
@NikosBaxevanis Это было бы невозможно без вашего сообщения в блоге. Я очень жду возможности увидеть, какой чистый DSL вы сможете придумать! : P (хотя я немного обеспокоен потерей сравнения имен участников, которое даст Likeness<T>). еще одна уловка Альбедо, несомненно, тоже сыграет свою роль! Это проблемное пространство определенно созрело для исследования! Я думаю, что также есть возможность использовать активные паттерны в миксе. Также большая заслуга принадлежит моему коллеге Адаму! - person Ruben Bartelink; 10.12.2013