我想使用列表、数组和/或 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属性?
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)
这也会对每个参数进行类型检查。
Add
替换为构造函数调用,例如:
type Values() =
inherit TheoryData<_, _>([
3, 4
32, 42 ])
[<Theory; ClassData(typeof<Values>)>]
let v2 (a : int, b : int) : unit = Assert.NotEqual(a, b)
如这个问题中所述,您只能将文字与
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)
尽管这需要一些手动编码,但您可以重复使用数据类,这在现实项目中似乎很有用,我们经常对相同的数据运行不同的测试。
您也可以使用不带类的成员数据:
let memberDataProperty = seq {
yield [|"param1":> Object; param2 :> Object; expectedResult :> Object |]
}
[<Theory; MemberData("memberDataProperty")>]
let ``Can use MemberData`` param1 param2 expectedResult = ...
您可以在这里使用
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[]>
。
一种可能性是使用 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)
进一步建立@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")