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



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

任何指示和提示都非常受赞赏!

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

下面的例子并不全面,但足以让你越过阈值并开始跑步。值得注意的是,测试函数是使用参数列表而不是柯里格式编写的。此外,还有几种使用 NUnit 3.x 属性生成测试数据的方法,例如成对,但遗憾的是,没有一个可用的属性知道如何为可区分的联合生成测试数据。

此外,不需要 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 编写的,因此它有点过时,但应该会给您一个理想的示例。

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

这来自我们中的一些人几年前做过的一个项目。

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 将 NUnit 与 F# 一起使用的问题是记住在正确的位置使用<>

在 NUnit3 中,有TestCaseSourceTestCaseData,对于最佳实践部分,我添加了 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可以获取和返回任意对象(显然它们应该与测试签名匹配)。此外,数据可以写得更好:

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

一天结束时,我意识到我一开始就不应该使用数组!

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

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

CLI 对属性参数的种类有限制:

  • 原语:布尔值、整数、浮点数等
  • 枚举
  • 字符串
  • 类型
  • 引用:系统类型。
  • "kinda 对象":从上面框(如果需要)表示类型
  • 上面一种类型的一维数组(即不允许嵌套数组)

因此,我们可以在这一点上得出结论,您不能将元组用作 属性参数的类型。

最新更新