F# 中 NUnit 3 测试的测试用例

Testcases for NUnit 3 tests in F#

我正在尝试使用 NUnit 为 F# 项目设置测试套件。 似乎尤其是在测试诸如解析器和类型检查器之类的东西时,通常会有一个有效输入数据列表和一个无效数据列表。 测试本身实际上是相同的,所以我正在寻找一种聪明的方法来避免为每个数据项编写测试函数,而是将测试函数与数据分开。 显然,似乎有一些叫做测试用例的东西,但我很难找到关于 NUnit 3 与 F# 一起使用的综合文档,特别是 我的场景的最佳实践示例

非常感谢任何指点和提示!

这是 NUnit 3.x 的更新答案,因为我的原始答案显示的是 NUnit 2.x 示例。

下面的示例并不全面,但足以让您跨过门槛并提高 运行。需要注意的是,测试函数是使用参数列表而不是柯里化编写的。还有几种方法可以使用 NUnit 3.x 属性生成测试数据,例如Pairwise,但遗憾的是 none 的可用属性知道如何为受歧视的联合生成测试数据。

也不需要 FSUnit,我也没有尝试让它工作,因为 NUnint 2.x 和 3.x 之间的区别是如此显着,我很高兴让下面的例子工作.也许以后我会更新这个答案。

namespace NUnit3Demo

open NUnit.Framework

module MyTest = 
    // ----------------------------------------------------------------------

    [<Pairwise>]
    let pairWiseTest([<Values("a", "b", "c")>] (a : string), [<Values("+", "-")>] (b :string), [<Values("x", "y")>] (c : string))
        = printfn "%A %A %A" a b c

    // ----------------------------------------------------------------------

    let divideCases1 =
        [
            12, 3, 4
            12, 2, 6
            12, 4, 3
        ] |> List.map (fun (q, n, d) -> TestCaseData(q,n,d))

    [<TestCaseSource("divideCases1")>]
    let caseSourceTest1(q:int, n:int, d:int) =
        Assert.AreEqual( q, n / d )

    // ----------------------------------------------------------------------

    let divideCases2 =
        seq {
            yield (12, 3, 4)
            yield (12, 2, 6)
            yield (12, 4, 3)
        }

    [<TestCaseSource("divideCases2")>]
    let caseSourceTest2(q:int, n:int, d:int) =
        Assert.AreEqual( q, n / d )

    // ----------------------------------------------------------------------

    [<TestCase(12,3,4)>]
    [<TestCase(12,2,6)>]
    [<TestCase(12,4,3)>]
    let testCaseTest(q:int, n:int, d:int) =
        Assert.AreEqual( q, n / d )

    // ----------------------------------------------------------------------

    let evenNumbers : int [] = [| 2; 4; 6; 8 |]

    [<TestCaseSource("evenNumbers")>]
    let caseSourceTest3 (num : int) =
        Assert.IsTrue(num % 2 = 0)

留下原始答案,因为它已被 OP 在其他答案中注明。

以下示例是 3 年前使用 NUnit 2.x 编写的,因此它有点过时,但应该会给您一个理想的结果。

您创建一个测试数据数组,然后对该数组进行索引以提取测试值和预期结果。这样做的好处是您不会为一个函数编写大量单独的测试。

这来自 project 我们中的一些人几年前做的。

open NUnit.Framework
open FsUnit

let private filterValues : (int list * int list)[] = [| 
    (
        // idx 0
        // lib.filter.001
        [],
        []
    ); 
    (
        // idx 1
        // lib.filter.002
        [-2],
        [-2]
    );
    (
        // idx 2
        // lib.filter.003
        [-1],
        []
    );
    (
        // idx 3
        // lib.filter.004
        [0],
        [0]
    );
    (
        // idx 4
        // lib.filter.005
        [1],
        []
    );
    (
        // idx 5
        // lib.filter.006
        [1; 2],
        [2]
    );
    (
        // idx 6
        // lib.filter.007
        [1; 3],
        []
    );
    (
        // idx 7
        // lib.filter.008
        [2; 3],
        [2]
    );
    (
        // idx 8
        // lib.filter.009
        [1; 2; 3],
        [2]
    );
    (
        // idx 9
        // lib.filter.010
        [2; 3; 4],
        [2; 4]
    );
    |]

[<Test>]
[<TestCase(0, TestName = "lib.filter.01")>]
[<TestCase(1, TestName = "lib.filter.02")>]
[<TestCase(2, TestName = "lib.filter.03")>]
[<TestCase(3, TestName = "lib.filter.04")>]
[<TestCase(4, TestName = "lib.filter.05")>]
[<TestCase(5, TestName = "lib.filter.06")>]
[<TestCase(6, TestName = "lib.filter.07")>]
[<TestCase(7, TestName = "lib.filter.08")>]
[<TestCase(8, TestName = "lib.filter.09")>]
[<TestCase(9, TestName = "lib.filter.10")>]
let ``List filter`` idx = 
    let (list, _) = filterValues.[idx]
    let (_, result) = filterValues.[idx]
    List.filter (fun x -> x % 2 = 0) list 
    |> should equal result
    filter (fun x -> x % 2 = 0) list 
    |> should equal result

IIRC 在 F# 中使用 NUnit 的问题是要记住在正确的位置使用 <>

到最后,我意识到我一开始就不应该使用数组!

我终于理解了 TestCase 机制应该如何工作: 它只是将注释的内容作为参数传递给现在不再是 unit->unit 但(在我的例子中)是 string->unit 的函数。因此,我所有的数据项现在都粘贴到单独的 TestCase 注释中,并且数组消失了。当然,这可能看起来有点奇怪,因为 TestCase 注释的内容跨越多行代码,但数组也不漂亮,就这样吧。

不幸的是,我的解决方案并不普遍适用,例如不适用于上面 Guy Coder 的代码。原因在这里指出: 他们说:

CLI has a restriction regarding kinds of attribute parameters:

  • primitive: bool, int, float, etc
  • enums
  • strings
  • type references: System.Type
  • 'kinda objects': boxed (if needed) representation of types from above
  • one dimensional array of one of types from above (ie. no nested arrays allowed)

So we can conclude at this point that you can not use tuple as an attribute parameter's type.

在 NUnit3 中有 TestCaseSource and TestCaseData and for the best practices part I added FsUnit:

namespace NUnit3Demo

open NUnit.Framework
open FsUnit

[<TestFixture>]
module MyTest = 

    let methodToBeTested s = 
        if String.length s > 3 then failwith "Something's wrong"
        else String.length s

    let validData =
        [
            TestCaseData("   ").Returns(3)
            TestCaseData("").Returns(0)
            TestCaseData("a").Returns(1)
        ]

    let invalidData =
        [
            "    "
            "abcd"
            "whatever"
        ]

    let otherInvalidData =
        [
            "just"
            "because"
        ]

    [<TestCaseSource("invalidData");
      TestCaseSource("otherInvalidData")>]
    let ``More than 3 characters throws`` s = 
        (fun () -> methodToBeTested s |> ignore)
        |> should throw typeof<System.Exception>

    [<TestCaseSource("validData")>]
    let ``Less than 4 characters returns length`` s = 
        methodToBeTested s

请注意 TestCaseData 可以接受和 return 任意对象(显然它们应该与测试签名匹配)。还有,数据可以写得更好:

let validData =
    [
        "   ", 3
        "",    0
        "a",   1
    ] |> List.map (fun (d, r) -> TestCaseData(d).Returns r)