Search code examples
.netf#tddxunit

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


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?


Solution

  • 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)