【问题标题】:How to check the case of a discriminated union with FsUnit?如何检查与 FsUnit 的歧视性工会的情况?
【发布时间】:2013-09-30 20:20:00
【问题描述】:

我想检查一个值是否属于可区分联合的特定情况,而不必检查任何包含的数据。我的动机是每个单元测试只测试一件事。

一个例子如下(最后两行给出编译错误):

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 中完成吗?

我知道this answer,但我不想为每种情况编写匹配函数(在我的实际代码中,不止两个)。

【问题讨论】:

  • 实际上有一个相当简单的方法可以从 C# 中做到这一点,但它在 F# 中不起作用。

标签: f# discriminated-union fsunit


【解决方案1】:

如果您不介意使用反射,this answer 中的 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

【讨论】:

    【解决方案2】:

    似乎 FSUnit 不(或者不能,我不确定)直接支持这个用例。

    我发现的下一个最好的事情是声明一个TestResult 类型,如下所示,并使用匹配将结果减少为这种类型。

    type TestResult =
    | Pass
    | Fail of obj
    

    这是减少匹配

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

    现在您可以使用should equal 来确保正确的结果。

    testResult  |> should equal Pass
    

    这种解决方案的好处是强类型,但更重要的是在失败的情况下,您可以看到无效的结果是什么

    【讨论】:

      【解决方案3】:

      看起来不是很优雅,但是可以从 state 的值中提取类型:

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

      然后在测试中使用:

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

      编辑

      是的,很遗憾,类型始终是 MyState。看起来模式匹配或丑陋的反射是不可避免的。

      【讨论】:

      • 这似乎不起作用 - 即使我使用 StateOne 88,测试也会通过,因此不会检查该值是否为 StateTwo
      • 这不起作用,因为类型 'a 将只是 MyState。如果instanceOfState 使用有关参数state 的运行时类型信息,它会起作用,但这意味着它需要以不同的方式工作......
      【解决方案4】:

      如果FsUnit 已经支持针对特定联合情况的断言,尽管仅限于Microsoft.FSharp.Core.Choice&lt;_,...,_&gt; 类型的值,该怎么办?

      让我们利用多案例活动模式来利用这一点,该模式使用反射来检查联合案例名称。

      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)
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-05-04
        • 2012-02-25
        • 2011-11-17
        • 2015-01-22
        • 1970-01-01
        • 2017-06-08
        • 1970-01-01
        • 2012-04-26
        相关资源
        最近更新 更多