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.

ClassDataMemberData不要求使用字面量,但是如果在运行器能显示每行,每个参数必须使用基元类型或基元数组类型。

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参数。