In F# how do you pass a collection to xUnit's InlineData attribute
Asked Answered
S

6

15

I would like to be about to use a list, array, and/or seq as a parameter to xUnit's InlineData.

In C# I can do this:

using Xunit; //2.1.0

namespace CsTests
{
    public class Tests
    {
        [Theory]
        [InlineData(new[] {1, 2})]
        public void GivenCollectionItMustPassItToTest(int[] coll)
        {
            Assert.Equal(coll, coll);
        }
    }
}

In F# I have this:

namespace XunitTests

module Tests =
  open Xunit //2.1.0

  [<Theory>]
  [<InlineData(8)>]
  [<InlineData(42)>]
  let ``given a value it must give it to the test`` (value : int) =
    Assert.Equal(value, value)

  [<Theory>]
  [<InlineData([1; 2])>]
  let ``given a list it should be able to pass it to the test``
  (coll : int list) =
    Assert.Equal<int list>(coll, coll)

  [<Theory>]
  [<InlineData([|3; 4|])>]
  let ``given an array it should be able to pass it to the test``
  (coll : int array) =
    Assert.Equal<int array>(coll, coll)

The F# code give the following build errors:

Library1.fs (13, 16): This is not a valid constant expression or custom attribute value

Library1.fs (18, 16): This is not a valid constant expression or custom attribute value

Referring to the 2nd and 3rd test theories.

Is it possible to use xUnit to pass in collections to the InlineData attribute?

Stygian answered 27/1, 2016 at 0:36 Comment(5)
Thanks James, if that is the case how do people normally hand needing a collection for test data in F#?Stygian
I'd suggested this is a duplicate of #29349652. However that question is specifically how to create a literal list in F# (and the answer is: you can't). This question is "How can I use xUnit and pass in a list of test data". Looks like @bytebuster has a workable answer.Lahnda
You can't use lists, but you ought to be able to use arrays like in C#, but I've never been able to make it compile. Fortunately, this becomes irrelevant once you discover FsCheck.Xunit.Maraschino
@MarkSeemann thanks. I was hoping there was a solution for example based testing, but maybe I need to use property based testing more.Stygian
This question shows other ways of achieving the same goal #22094343Watthour
F
21

InlineDataAttribute leans on the C# params mechanism. This is what enables the default syntax of InlineData in C# :-

[InlineData(1,2)]

Your version with array construction:-

[InlineData( new object[] {1,2})]

is simply what the compiler translates the above into. The minute you go further, you'll run into the same restrictions on what the CLI will actually enable - the bottom line is that at the IL level, using attribute constructors implies that everything needs to be boiled down to constants at compile time. The F# equivalent of the above syntax is simply: [<InlineData(1,2)>], so the direct answer to your question is:

module UsingInlineData =
    [<Theory>]
    [<InlineData(1, 2)>]  
    [<InlineData(1, 1)>]  
    let v4 (a : int, b : int) : unit = Assert.NotEqual(a, b)

I was unable to avoid riffing on @bytebuster's example though :) If we define a helper:-

type ClassDataBase(generator : obj [] seq) = 
    interface seq<obj []> with
        member this.GetEnumerator() = generator.GetEnumerator()
        member this.GetEnumerator() = 
            generator.GetEnumerator() :> System.Collections.IEnumerator

Then (if we are willing to forgo laziness), we can abuse list to avoid having to use seq / yield to win the code golf:-

type MyArrays1() = 
    inherit ClassDataBase([ [| 3; 4 |]; [| 32; 42 |] ])

[<Theory>]
[<ClassData(typeof<MyArrays1>)>]
let v1 (a : int, b : int) : unit = Assert.NotEqual(a, b)

But the raw syntax of seq can be made sufficiently clean, so no real need to use it as above, instead we do:

let values : obj[] seq = 
    seq { 
        yield [| 3; 4 |] 
        yield [| 32; 42 |] // in recent versions of F#, `yield` is optional in seq too
    }

type ValuesAsClassData() = 
    inherit ClassDataBase(values)

[<Theory; ClassData(typeof<ValuesAsClassData>)>]
let v2 (a : int, b : int) : unit = Assert.NotEqual(a, b)

However, most idiomatic with xUnit v2 for me is to use straight MemberData (which is like xUnit v1's PropertyData but generalized to also work on fields) :-

[<Theory; MemberData("values")>]
let v3 (a : int, b : int) : unit = Assert.NotEqual(a, b)

The key thing to get right is to put the : seq<obj> (or : obj[] seq) on the declaration of the sequence or xUnit will throw at you.


Later versions of xUnit 2 include a typed TheoryData, which lets you write:

type Values() as this =
    inherit TheoryData<int,int>()
    do  this.Add(3, 4)
        this.Add(32, 42)

[<Theory; ClassData(typeof<Values>)>]
let v2 (a : int, b : int) : unit = Assert.NotEqual(a, b)

That also type-checks each argument.

Current xUnit v2 releases let you replace the Add with constructor calls, e.g.:

type Values() =
    inherit TheoryData<_, _>([
        3, 4
        32, 42 ])

[<Theory; ClassData(typeof<Values>)>]
let v2 (a : int, b : int) : unit = Assert.NotEqual(a, b)
Foe answered 1/2, 2016 at 10:19 Comment(2)
Thanks this works, but NCrunch does not seem to like MemberData. O-well, I'll most likely either use a Fact or FsCheck going forward.Stygian
Is it Theory or MemberData it doesnt like? If its the latter, then you should be able to make PropertyData work - just add with get() to the field? You're right about having ample substitutes - in general it's easy enough to make each leg of a Theory an individually addressable Fact and probably end up with more readable code.Foe
M
10

As described in this question, you can only use literals with InlineData. Lists are not literals.

However, xUnit provides with ClassData which seems to do what you need.

This question discusses the same problem for C#.

In order to use ClassData with the tests, just make a data class implementing seq<obj[]>:

type MyArrays () =    
    let values : seq<obj[]> = seq {
       [|3; 4|]    // 1st test case
       [|32; 42|]  // 2nd test case, etc.
    }
    interface seq<obj[]> with
        member this.GetEnumerator () = values.GetEnumerator()
        member this.GetEnumerator () =
            values.GetEnumerator() :> System.Collections.IEnumerator

module Theories = 
    [<Theory>]
    [<ClassData(typeof<MyArrays1>)>]
    let ``given an array it should be able to pass it to the test`` (a : int, b : int) : unit = 
        Assert.NotEqual(a, b)

Albeit this requires some manual coding, you may re-use the data class, which appears to be useful in real-life projects, where we often run different tests against the same data.

Misericord answered 27/1, 2016 at 1:50 Comment(3)
I cannot get this solution to work as is, but I see where you are going and believe this the "correct" answer. Thank you I will make it as correct once I get it to work.Stygian
The seq { [|3; 4|] [|32; 43|] } is not a valid seq expression, I ended up using type MyArrays () = let values = seq [ [|3; 4|] // 1st test case [|32; 42|] // 2nd test case, etc. ] |> Seq.cast<obj> interface seq<obj[]> with member this.GetEnumerator<'T> () = values.GetEnumerator() :> System.Collections.Generics.IEnumerator<'T> member this.GetEnumerator () = values.GetEnumerator() :> System.Collections.IEnumeratorStygian
@MikeHarris You are correct in your suppositions, I've edited in two missing yield operators and a type mistake [And an answer expanding on all this below]Foe
R
6

You can also use the member data without class:

let memberDataProperty = seq {
    yield [| box "param1"; box param2; box expectedResult |]
}

[<Theory; MemberData(nameof memberDataProperty)>]
let ``Can use MemberData`` param1 param2 expectedResult = ...
Rosebud answered 18/12, 2018 at 22:58 Comment(1)
nice - some tweaks 1) you can use box instead of :> Object 2) if using F# 4.7, you can use nameof to derive "memberDataProperty"Foe
L
5

You can use the FSharp.Reflection namespace to good effect here. Consider some hypothetical function isAnswer : (string -> int -> bool) that you want to test with a few examples.

Here's one way:

open FSharp.Reflection
open Xunit

type TestData() =
  static member MyTestData =
    [ ("smallest prime?", 2, true)
      ("how many roads must a man walk down?", 41, false) 
    ] |> Seq.map FSharpValue.GetTupleFields

[<Theory; MemberData("MyTestData", MemberType=typeof<TestData>)>]
let myTest (q, a, expected) =
  Assert.Equals(isAnswer q a, expected)

The key thing is the |> Seq.map FSharpValue.GetTupleFields line. It takes the list of tuples (you have to use tuples to allow different arguments types) and transforms it to the IEnumerable<obj[]> that XUnit expects.

Lilas answered 18/6, 2018 at 8:7 Comment(0)
P
3

One possibility is to use xUnit's MemberData attribute. A disadvantage with this approach is that this parameterized test appears in Visual Studio's Test Explorer as one test instead of two separate tests because collections lack xUnit's IXunitSerializable interface and xUnit hasn't added build-in serialization support for that type either. See xunit/xunit/issues/429 for more information.

Here is a minimal working example.

module TestModule

  open Xunit

  type TestType () =
    static member TestProperty
      with get() : obj[] list =
        [
          [| [0]; "a" |]
          [| [1;2]; "b" |]
        ]

    [<Theory>]
    [<MemberData("TestProperty")>]            
    member __.TestMethod (a:int list) (b:string) =
      Assert.Equal(1, a.Length)

See also this similar question in which I give a similar answer.

Pentheus answered 31/7, 2019 at 3:38 Comment(0)
H
3

Building further on @Assassin's brilliant answer -- now we have implicit yields you can put the test cases in an array and dispense with the yields. I would also be tempted to add a cheeky little private operator to handle the object conversions. Thus:

open System
open Xunit

let inline private (~~) x = x :> Object

let degreesToRadiansCases =
    [|
        // Degrees; Radians
        [|   ~~0.0;            ~~0.0  |]
        [| ~~360.0; ~~(Math.PI * 2.0) |]
    |]

[<Theory>]
[<MemberData("degreesToRadiansCases")>]
let ``Convert from degrees to radians`` (degrees, radians) =
    let expected = radians
    let actual = Geodesy.Angle.toRadians degrees
    Assert.Equal(expected, actual)

let stringCases =
    [|
        [| ~~99; ~~"hello1" |] 
        [| ~~99; ~~"hello2" |] 
    |]

[<Theory>]
[<MemberData("stringCases")>]
let ``tests`` (i, s) =
    printfn "%i %s" i s
    Assert.Equal(s, "hello1")
Hatcher answered 23/1, 2020 at 21:52 Comment(1)
You can drop the ~~ operator if you use degreesToRadiansCases: Object [] [], as shown in Tyson's answer.Sweeny

© 2022 - 2024 — McMap. All rights reserved.