如何使用 xUnit 测试 F# 选项类型

How to test F# option types with xUnit

我想在 F# 中使用 xUnit 对这段代码进行单元测试。如何处理选项?

摘自 Scott Wlaschin 的书:领域建模功能化

type UnitQuantity = private UnitQuantity of int
// ^ private constructor

// define a module with the same name as the type
module UnitQuantity =
    /// Define a "smart constructor" for UnitQuantity
    /// int -> Result<UnitQuantity,string>
    let create qty =
        if qty < 1 then
            // failure
            Error "UnitQuantity can not be negative"
        else if qty > 1000 then
            // failure
            Error "UnitQuantity can not be more than 1000"
        else
            // success -- construct the return value
            Ok (UnitQuantity qty)

测试:

let ``Check UnitQuantity.create is one`` () =

    // ARRANGE
    let expected = 1

    // ACT
    //let unitQtyResult = UnitQuantity.create 1
    match UnitQuantity.create 1 with
    | Error msg -> 0
        //printfn "Failure, Message is %s" msg
    | Ok x -> 0
       // let innerValue = UnitQuantity.value actual

    // ASSERT
    //Assert.Equal(expected,actual)

我知道 ACT 完全错了,这就是我被挂断的地方。我对 F# 选项、xUnit.net 和单元测试的理解都不够,无法断言函数的实际值。

单元测试的一般经验法则是 Act 部分应该是单个语句。

我们要检查的所有结果都是某种形式的断言

所以我们要断言结果是 Ok<UnitQuantity> 还是 Error<string>

这是模式匹配允许我们非常简洁地测试它的地方

let ``Check UnitQuantity.create is one`` () =
    // ARRANGE
    let qty = 1 // The quantity we supply is one
    let expected = qty // We expect to get that value back

    // ACT
    let actual = UnitQuantity.create qty

    // ASSERT

    // Check if we have an Ok or an Error
    match actual with
      | Ok unitQuantity ->
        // If Ok, check the value is what we expect
        let innerValue = UnitQuantity.value unitQuantity
        Assert.Equal(innerValue, expected)

      // If Error raise an AssertException to fail the test
      | Error errorMessage ->
        let error = sprintf "Expected Ok, was Error(%s)." errorMessage
        Assert.True(false, error) // Force an assertion failure with our error message

注意 UnitQuantity.value 方法,这是一个简单的解包函数,您可以将其添加到 UnitQuantity 模块的末尾,它将返回 int 值,这样您就可以轻松地比较一下

let value (UnitQuantity e) = e

如果你想测试一个 option 类型,它会非常相似,使用像这样的匹配语句

match actual with
  | Some value ->
    // Do your assertions here
    ()
  | None ->
    // Do your assertions here
    ()

我可能会直接比较结果而不是模式匹配。但是,由于 private 构造函数,您无法为 Result<UnitQuantity, string> 创建 Ok 结果。

您可以使用内置的 Result.map 映射结果的 Ok 值。使用 UnitQuantity.value 可以从 Result<UnitQuantity, string> 映射到 Result<int, string>。所以这应该有效:

let expected = Ok 1

let actual = UnitQuantity.create 1 |> Result.map UnitQuantity.value

Assert.Equal(expected, actual)

面对类似的问题,我决定我不太关心检查值本身,而是检查高于和低于阈值的行为。我决定验证结果是否为 Ok 如果值在范围内并且 Error 超出范围。

type UnitQuantity = private UnitQuantity of int

module UnitQuantity =

    let min = 1
    let max = 1000

    let create qty =
        if qty < min then
            Error $"UnitQuantity cannot be less than {min}"
        else if qty > max then
            Error $"UnitQuantity cannot be more than {max}"
        else
            Ok (UnitQuantity qty)

module Tests =

    let min = 1
    let max = 1000

    let shouldBeError = function
        | Error _ -> ()
        | _ -> failwith "is not error"

    let shouldBeOk = function
        | Ok _ -> ()
        | _ -> failwith "is not Ok"

    [<Fact>]
    let ``create unit qty at min`` () =
        UnitQuantity.create min |> shouldBeOk

    [<Fact>]
    let ``create unit qty below min`` () =
        UnitQuantity.create (min - 1) |> shouldBeError

    [<Fact>]
    let ``create unit qty at max`` () =
        UnitQuantity.create max |> shouldBeOk

    [<Fact>]
    let ``create unit qty above max`` () =
        UnitQuantity.create (max + 1) |> shouldBeError