Как проверить случай дискриминируемого союза с FsUnit?

Я хотел бы проверить, что значение относится к конкретному случаю размеченного союза, без необходимости также проверять какие-либо включенные данные. Моя мотивация состоит в том, чтобы тестировать только одну вещь с каждым модульным тестом.

Пример такой (последние две строки дают ошибки компиляции):

module MyState

open NUnit.Framework
open FsUnit

type MyState =
    | StateOne of int
    | StateTwo of int

let increment state =
    match state with
    | StateOne n when n = 10 -> StateTwo 0
    | StateOne n -> StateOne (n + 1)
    | StateTwo n -> StateTwo (n + 1)

[<Test>]
let ``incrementing StateOne 10 produces a StateTwo`` ()=
    let state = StateOne 10
    (increment state) |> should equal (StateTwo 0)             // works fine
    (increment state) |> should equal (StateTwo _)             // I would like to write this...
    (increment state) |> should be instanceOfType<StateTwo>    // ...or this

Можно ли это сделать в FsUnit?

Мне известен этот ответ, но я бы предпочел не писать соответствующие функции для каждого случая (в моем реальном коде есть намного больше двух).


person Mark Pattison    schedule 25.09.2013    source источник
comment
На самом деле есть достаточно простой способ сделать это из C#, но он не работает в F#.   -  person John Palmer    schedule 25.09.2013


Ответы (4)


Если вы не возражаете против использования отражений, вам может пригодиться функция isUnionCase из этого ответа:

increment state 
|> isUnionCase <@ StateTwo @>
|> should equal true

Обратите внимание, что это немного многословно, потому что вам нужен вызов функции перед сравнением значений.

Похожим, но более легким подходом может быть сравнение тегов:

// Copy from https://stackoverflow.com/a/3365084
let getTag (a:'a) = 
  let (uc,_) = Microsoft.FSharp.Reflection.FSharpValue.GetUnionFields(a, typeof<'a>)
  uc.Name

increment state 
|> getTag
|> should equal "StateTwo"

Имейте в виду, что это небезопасно для типов, и вы можете легко ошибиться в имени регистра объединения.

Что бы я сделал, так это создал бы аналогичный DU для сравнения:

type MyStateCase =
    | StateOneCase
    | StateTwoCase

let categorize = function
    | StateOne _ -> StateOneCase
    | StateTwo _ -> StateTwoCase

Таким образом, вы определяете categorize один раз и используете его несколько раз.

increment state
|> categorize
|> should equal StateTwoCase
person pad    schedule 25.09.2013

Похоже, что FSUnit не поддерживает (или не может, я не уверен) напрямую этот вариант использования.

Следующее лучшее, что я нашел, - это объявить тип TestResult, как показано ниже, и использовать совпадение, чтобы привести результат к этому типу.

type TestResult =
| Pass
| Fail of obj

Вот уменьшающая спичка

let testResult =
    match result with
    | OptionA(_) -> Pass
    | other -> Fail(other)

Теперь вы можете просто использовать should equal, чтобы обеспечить правильный результат.

testResult  |> should equal Pass

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

person Daniel Little    schedule 24.11.2016

Выглядит не очень элегантно, но вы можете извлечь тип из значения состояния:

let instanceOfState (state: 'a) =
    instanceOfType<'a>

А затем используйте его в тесте:

(increment state) |> should be (instanceOfState <| StateTwo 88)

ИЗМЕНИТЬ

Да, к сожалению, тип всегда MyState. Похоже, сопоставление с образцом или уродливое отражение неизбежны.

person Eugene Fotin    schedule 25.09.2013
comment
Похоже, это не работает - тест проходит, даже если я использую StateOne 88, поэтому не проверяет, является ли значение StateTwo. - person Mark Pattison; 25.09.2013
comment
Это не работает, потому что тип 'a будет просто MyState. Это сработало бы, если бы instanceOfState использовал информацию о типе времени выполнения для параметра state, но это означает, что он должен работать немного по-другому... - person Tomas Petricek; 25.09.2013

Что, если FsUnit уже поддерживает утверждение против определенного случая объединения, хотя и ограниченное значениями типа Microsoft.FSharp.Core.Choice<_,...,_>?

Давайте воспользуемся этим с активным шаблоном с несколькими регистрами, который использует Reflection для проверки имени случая объединения.

open System.Reflection
open Microsoft.FSharp.Reflection

let (|Pass|Fail|) name (x : obj) =
    let t = x.GetType()
    if FSharpType.IsUnion t &&
        t.InvokeMember("Is" + name,
            BindingFlags.GetProperty, null, x, null )
        |> unbox then Pass
    else Fail x

Должно работать сейчас:

increment state
|> (|Pass|Fail|) "StateTwo"
|> should be (choice 1)
person kaefer    schedule 24.11.2016