Как проверить случай дискриминированного союза с 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?

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

4 ответа

Решение

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

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

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

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

// Copy from https://stackru.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

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

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

type TestResult =
| Pass
| Fail of obj

Вот сокращающий матч

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

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

testResult  |> should equal Pass

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

Что делать, если 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)

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

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

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

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

РЕДАКТИРОВАТЬ

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

Другие вопросы по тегам