Parameterized xUnit Tests with F#
InlineData
最简单用法只有一个整数型循环变量,用一个列表值保存数据:
[<Theory>]
[<InlineData(0)>]
[<InlineData(1)>]
member _.``02 - augment production test``(dot:int) =
let e = [
{production= ["";"s"];dot= 0;backwards= [];forwards= ["s"];dotmax= false;isKernel= true};
{production= ["";"s"];dot= 1;backwards= ["s"];forwards= [];dotmax= true;isKernel= true}]
let i = e.[dot]
let x = {production= i.production;dot= i.dot}
Should.equal x.backwards i.backwards
Should.equal x.forwards i.forwards
Should.equal x.dotmax i.dotmax
Should.equal x.isKernel i.isKernel
可变参数:
open Xunit
open Xunit.Abstractions
type ParameterizedxUnitTest(output:ITestOutputHelper) =
[<Theory>]
[<InlineData(1, 42, 43)>]
[<InlineData(1, 2, 3)>]
member _.``add 1 2 equals 3``(a: int, b: int, expected: int) =
let actual = a + b
Assert.Equal(expected, actual)
测试成员的参数表数目与InlineData
特性的实参数目应该匹配,否则测试资源管理器会丢失相关一些测试类。
InlineData
的限制是只能使用字面量常数。因为属性的参数必须是字面量。
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.
而ClassData
和MemberData
不要求使用字面量,但是如果在运行器能显示每行,每个参数必须使用基元类型或基元数组类型。
ClassData
用法是定义一个类型,其继承自ClassDataBase
,其定义在FSharp.xUnit
中。
open FSharp.xUnit
type MyArrays1() =
inherit ClassDataBase([
[| 3; 4 |];
[| 32; 42 |]
])
type ClassDataBaseTest(output:ITestOutputHelper) =
[<Theory>]
[<ClassData(typeof<MyArrays1>)>]
member _.v1 (a : int, b : int) =
Assert.NotEqual(a, b)
MemberData
However, most idiomatic with xUnit for me is to use straight MemberData
. 因为参数表每个参数可以有不同的类型,you have to use tuples to allow different arguments types. You can use the FSharp.Reflection
namespace to good effect here.
引用相同类型,请提供成员的名称:
type MemberDataTest(output:ITestOutputHelper) =
static member samples =
[
[|"Homer";""|],"Homer"
[|"Marge";""|],"Marge"
]
|> Seq.map FSharpValue.GetTupleFields
[<Theory>]
[<MemberData(nameof(MemberDataTest.samples))>]
member _.``array different types``(a:string[], b) =
let c = a.[0]
Assert.Equal(c, b)
引用不同类型中的数据,请提供参数MemberType
:
open FSharp.Reflection
type TestData() =
static member MyTestData =
[
"smallest prime?", 2, true
"how many roads must a man walk down?", 41, false
]
|> Seq.map FSharpValue.GetTupleFields
type MemberDataTest(output:ITestOutputHelper) =
[<Theory; MemberData("MyTestData", MemberType=typeof<TestData>)>]
member _.myTest(q, a, expected) =
let isAnswer (q:string) a =
q.Split(" ").Length = a
Assert.Equal(isAnswer q a, expected)
上面两个示例,The key thing is the line
|> Seq.map FSharpValue.GetTupleFields
It takes the list of tuples and transforms it to the seq<obj[]>
that XUnit expects.
最佳用法
此用例,使用词典绕过xUnit不支持的数据类型,词典中的键作为内联数据的参数表,测试函数所需的其他数据,使用键在词典中查询。键在“测试资源管理器”显示MemberData
多行。同InlineData
一样,测试资源管理器只能使用基元数据类型组成的数组。
type UnifyVoidElementTest(output:ITestOutputHelper) =
static let source = [
"<br/>",[{index= 0;length= 5;value= TagSelfClosing("br",[])}]
"<p><br></br></p>",[{index= 0;length= 3;value= TagStart("p",[])};{index= 3;length= 4;value= TagSelfClosing("br",[])};{index= 12;length= 4;value= TagEnd "p"}]
]
static let mp = Map.ofList source
static member keys =
source
|> Seq.map (fst>>Array.singleton)
[<Theory;MemberData(nameof UnifyVoidElementTest.keys)>]
member _.``self closing``(x:string) =
let y =
x
|> SeniorTokenizer.tokenize
|> Seq.choose (HtmlTokenSeniorUtils.unifyVoidElement)
|> Seq.toList
let expec = mp.[x]
Should.equal expec y
此例中,所有的数据都收集在source
中,构成一个行列表。每行是一个记录元组,第一项是输入数据,字符串类型。第二项输出数据,复杂类型。且第一项就是元组的键。Theory
所需要的keys
仅取第一项,因为每个键是xunit支持的类型,可以Theory分行显示,而剩余的数据部分,在测试方法中通过mp.[x]
查询Map来获得。
keys
代表参数表是数组类型,即使是单个参数也要包装成数组。使用了Array.singleton
包装以匹配参数表的接口。
而在取用时x
是参数表里的单个参数。定义mp
时,从源头来没有包装,所以主键无需包装成数组。
封装最佳用法,利用SingleDataSource
安装NuGet包:
Install-Package FSharp.xUnit
这个包封装了最佳用法,提供了一个类SingleDataSource
。你需要向SingleDataSource
构造函数提供一个键值对列表,成员keys
提供给MemberData
,索引属性提供额外数据字段。
namespace FSharp.xUnit
open Xunit
open Xunit.Abstractions
open FSharp.xUnit
type SingleDataSourceTest(output:ITestOutputHelper) =
//* ctor
static let ds = SingleDataSource[
0,[]
1,[()]
2,[();()]
]
//MemberData args
static member keys = ds.keys
[<Theory>]
[<MemberData(nameof SingleDataSourceTest.keys)>]
member _.``unit list test`` (x:int) =
let y = List.replicate x ()
let e = ds.[x] //*
Should.equal e y
利用SingleDataSource且数据源位于测试类型以外的不同类型
type DS() =
static let ds = SingleDataSource[
"<!DOCTYPE HTML>",{index= 0;length= 15;value= DOCTYPE "HTML"}
]
static member keys = ds.keys
static member get key = ds.[key]
type ConsumptionTest(output:ITestOutputHelper) =
[<Theory>]
[<MemberData(nameof DS.keys, MemberType=typeof<DS>)>]
member _.``01 = DS``(x:string) =
let postok = Consumption.DS 0 x
output.WriteLine(stringify postok)
let e = DS.get x
Should.equal postok e
当数据源位于测试类型以外的不同类型的时候,需要给出MemberType
参数。