Как проверить случай дискриминированного союза с 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. Похоже, что сопоставление с образцом или уродливое отражение неизбежны.