How to check the case of a discriminated union with FsUnit?
Asked Answered
T

4

9

I'd like to check that a value is of a particular case of a discriminated union, without having to also check any included data. My motivation is to only test one thing with each unit test.

An example is as follows (the last two lines give compilation errors):

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

Can this be done in FsUnit?

I'm aware of this answer but would prefer not to have to write matching functions for each case (in my real code there are far more than two).

Tirrell answered 25/9, 2013 at 9:49 Comment(2)
There is actually a reasonably easy way to do this from C#, but it doesn't work in F#.Weevil
FYI: meanwhile FsUnit has the ofCase operator, as explained in the doc.Delenadeleon
C
9

If you don't mind using reflections, the isUnionCase function from this answer could be handy:

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

Note that it's a bit verbose because you need a function call before comparing values.

A similar but lighter approach could be comparison of tags:

// 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"

Beware that this is not type-safe and you can easily misspell a union case name.

What I would do is to create a similar DUs for comparison purpose:

type MyStateCase =
    | StateOneCase
    | StateTwoCase

let categorize = function
    | StateOne _ -> StateOneCase
    | StateTwo _ -> StateTwoCase

In this way, you define categorize once and use it multiple times.

increment state
|> categorize
|> should equal StateTwoCase
Clava answered 25/9, 2013 at 13:10 Comment(1)
FYI: meanwhile FsUnit has the ofCase operator, as explained in the doc.Delenadeleon
P
3

It appears FSUnit doesn't (or can't, I'm not sure) directly support this use case.

The next best thing I've found is to declare a TestResult type like the following and use a match to reduce the result to this type.

type TestResult =
| Pass
| Fail of obj

Here is the reducing match

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

Now you can just use should equal to ensure the correct result.

testResult  |> should equal Pass

The benefits of this solution are strong typing but more importantly in the failure case you can see what the invalid result was.

Parrie answered 24/11, 2016 at 5:23 Comment(0)
S
2

What if FsUnit already supports an assertion against a specific union case, albeit one restricted to values of the type Microsoft.FSharp.Core.Choice<_,...,_>?

Let's leverage this with a multi-case active pattern, which uses Reflection to check against the union case name.

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

Should be working now:

increment state
|> (|Pass|Fail|) "StateTwo"
|> should be (choice 1)
Squid answered 24/11, 2016 at 20:38 Comment(0)
T
0

It doesn't look very elegant, but you can extract type from a value of state:

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

And then use it in the test:

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

EDIT

Yes, unfortunately the type is always MyState. Looks like pattern matching or ugly reflection are inevitable.

Telekinesis answered 25/9, 2013 at 11:51 Comment(2)
This doesn't appear to work - the test passes even if I use StateOne 88, so doesn't check that the value is a StateTwo.Tirrell
This does not work because the type 'a will be just MyState. It would work if instanceOfState used runtime type information about the parameter state, but that means it needs to work a bit differently...Kornegay

© 2022 - 2024 — McMap. All rights reserved.