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

问题描述 投票:0回答:6

我想使用列表、数组和/或 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):这不是有效的常量表达式或自定义属性值

Library1.fs (18, 16):这不是有效的常量表达式或自定义属性值

参考第2、3次测试理论。

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

.net f# tdd xunit
6个回答
18
投票

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
来赢得代码高尔夫:-

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)

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

当前的 xUnit v2 版本允许您将

Add
替换为构造函数调用,例如:

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

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

10
投票

这个问题中所述,您只能将文字与

InlineData
一起使用。列表不是文字。

但是,xUnit 提供了

ClassData
,它似乎可以满足您的需要。

这个问题讨论了 C# 的相同问题。

为了在测试中使用

ClassData
,只需创建一个实现
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)

尽管这需要一些手动编码,但您可以重复使用数据类,这在现实项目中似乎很有用,我们经常对相同的数据运行不同的测试。


6
投票

您也可以使用不带类的成员数据:

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

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

5
投票

您可以在这里使用

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[]>


3
投票

一种可能性是使用 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)

另请参阅此类似问题,其中我给出了类似答案


3
投票

进一步建立@Assassin 的精彩答案——现在我们有了隐式收益,您可以将测试用例放入数组中并省去

yield
。我还想添加一个厚脸皮的小私有运算符来处理对象转换。因此:

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")
© www.soinside.com 2019 - 2024. All rights reserved.