简体   繁体   中英

How to test F# option types with xUnit

I want to unit test this bit of code using xUnit in F#. How do I handle the options?

From Scott Wlaschin's book: Domain Modeling Made Functional

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)

Test:

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)

I know the ACT is all wrong, that is where I am hung up. I do not understand F# options nor xUnit.net nor unit testing enough yet to Assert on the actual value from the function.

I'd probably compare the result directly rather than pattern matching. However, you can't create an Ok result for Result<UnitQuantity, string> because of the private constructor.

You can use the built-in Result.map to map the Ok value of a result. Using UnitQuantity.value you can map from Result<UnitQuantity, string> to Result<int, string> . So this should work:

let expected = Ok 1

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

Assert.Equal(expected, actual)

A general rule of thumb with unit tests is that the Act section should be a single statement.

Everything that we want to check about the result is some form of assertion

So we want to assert whether the result is either an Ok<UnitQuantity> or an Error<string> .

This is where pattern matching allows us to test this very succintly

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

Note the UnitQuantity.value method, it' a simple unwrap function you can add to the end of the UnitQuantity module that will give you back the int value so you can easily compare it

let value (UnitQuantity e) = e

If you wanted to test an option type, it would be very similar, using a match statement like so

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

Faced with a similar problem, I decided I wasn't so concerned with checking the value itself, but with checking the behavior above and below the threshold. I decided to verify the result is Ok if the value is in range and an Error outside the range.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM