在 F# 中,如何将集合传递给 xUnit 的 InlineData 属性

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

我想使用列表、数组、and/or seq 作为 xUnit 的 InlineData 的参数。

在 C# 中我可以这样做:

using Xunit; //2.1.0

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

在 F# 中我有这个:

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)

F# 代码给出以下构建错误:

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

参考第二和第三个测试理论。

是否可以使用 xUnit 将集合传递给 InlineData 属性?

中所述,您只能将文字与 InlineData 一起使用。列表不是文字。

但是,xUnit 提供了 ClassData,这似乎可以满足您的需要。

This question 讨论了 C# 的相同问题。

为了在测试中使用 ClassData,只需制作一个数据 class 实现 seq<obj[]>:

type MyArrays () =    
    let values : seq<obj[]>  =
        seq {
            yield [|3; 4|]    // 1st test case
            yield [|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)

尽管这需要一些手动编码,但您可以 re-use 数据 class,这在 real-life 项目中似乎很有用,我们经常在这些项目中 运行 进行不同的测试针对相同的数据。

InlineDataAttribute 依赖于 C# params 机制。这就是在 C# 中启用 InlineData 的默认语法的原因:-

[InlineData(1,2)]

你的数组构造版本:-

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

只是编译器将上面的内容翻译成的内容。在你更进一步的那一刻,你将 运行 进入对 CLI 实际启用的相同限制 - 底线是在 IL 级别,使用属性构造函数意味着一切都需要归结为常量编译时间。上述语法的 F# 等价物很简单:[<InlineData(1,2)>],因此您的问题的直接答案是:

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

虽然我无法避免重复@bytebuster 的示例 :) 如果我们定义一个助手:-

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

然后(如果我们愿意放弃懒惰),我们可以滥用 list 来避免必须使用 seq / yield 来赢得代码 golf:-

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

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

但是 seq 的原始语法可以变得足够干净,所以没有必要像上面那样使用它,而是我们这样做:

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)

然而,对我来说,xUnit v2 最惯用的是直接使用 MemberData(类似于 xUnit v1 的 PropertyData,但一般化后也适用于字段):-

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

正确的关键是将 : seq<obj>(或 : obj[] seq)放在序列的声明中,否则 xUnit 会抛出你。


xUnit 2 的更高版本包含一个类型化的 TheoryData,它允许您编写:

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)

这也会对每个参数进行类型检查。

这里可以使用FSharp.Reflection命名空间,效果不错。考虑一些您想用几个示例测试的假设函数 isAnswer : (string -> int -> bool)

这是一种方法:

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)

关键是 |> Seq.map FSharpValue.GetTupleFields 行。 它获取元组列表(您必须使用元组来允许不同的参数类型)并将其转换为 XUnit 期望的 IEnumerable<obj[]>

您也可以在没有class的情况下使用会员数据:

let memberDataProperty = seq {
    yield [|"param1":> Object; param2 :> Object; expectedResult :> Object |]
}

[<Theory; MemberData("memberDataProperty")>]
let ``Can use MemberData`` param1 param2 expectedResult = ...

一种可能是使用 xUnit 的 MemberData 属性。这种方法的一个缺点是这个参数化测试作为一个测试而不是两个单独的测试出现在 Visual Studio 的测试资源管理器中,因为集合缺少 xUnit 的 IXunitSerializable 接口并且 xUnit 没有添加内置序列化支持对于那种类型。有关详细信息,请参阅 xunit/xunit/issues/429

这是一个最小的工作示例。

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)

另请参阅此 similar question in which I give a similar answer

在@Assassin 的精彩回答的基础上进一步构建——现在我们有了隐式收益,您可以将测试用例放在一个数组中并省去 yields。我也很想添加一个厚颜无耻的小私有运算符来处理对象转换。因此:

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