使用 dotnet 测试和 TUnit 对 F# 库进行单元测试

本教程逐步讲解了构建示例解决方案的交互式体验,以学习单元测试概念。

本文介绍如何测试 .NET Core 项目。 如果要测试 ASP.NET Core 项目,请参阅 ASP.NET Core中的 集成测试。

Prerequisites

  • .NET 10 SDK 或更高版本。
  • 你选择的文本编辑器或代码编辑器。

TUnit 完全基于 Microsoft.Testing.Platform (MTP),不支持 VSTest。 本教程使用 dotnet test 的 MTP 模式,该模式需要 .NET 10 SDK。

创建源项目

打开 shell 窗口。 创建名为 unit-testing-with-fsharp 的目录来保存解决方案。 在此新目录中,运行以下命令,为类库和测试项目创建新的解决方案文件:

dotnet new sln

接下来,创建 MathService 目录。 以下大纲显示到目前为止的目录和文件结构:

/unit-testing-with-fsharp
    unit-testing-with-fsharp.slnx
    /MathService

将 MathService 设为当前目录并运行以下命令以创建源项目:

dotnet new classlib -lang "F#"

将 Library.fs 的内容替换为数学服务的一个会失败的实现:

namespace MathService

module MyMath =
    let squaresOfOdds (xs: int seq) : int seq = raise (System.NotImplementedException("You haven't written a test yet!"))

将目录更改回 unit-testing-with-fsharp 目录。 运行以下命令,将类库项目添加到解决方案:

dotnet sln add ./MathService/MathService.fsproj

创建测试项目

接下来,创建 MathService.Tests 目录。 以下大纲显示了目录结构:

/unit-testing-with-fsharp
    unit-testing-with-fsharp.slnx
    /MathService
        Source Files
        MathService.fsproj
    /MathService.Tests

.NET SDK 中不包含 TUnit 项目模板。 使用以下命令安装它们:

dotnet new install TUnit.Templates

将 MathService.Tests 目录设为当前目录,并使用以下命令创建新项目:

dotnet new TUnit -lang "F#"

Note

模板短名称区分大小写。 使用 TUnit,而不是 tunit。

此命令创建一个使用 TUnit 作为测试框架的测试项目。 生成的 MathService.Tests.fsproj 文件是一个可执行项目,它引用 TUnit 包,其中包括测试框架、断言库和 MTP 测试运行程序,以及 TUnit.Assertions.FSharp 包,该包为断言添加 F# 帮助程序。

该模板还会添加示例测试。 本教程在 Tests.fs 中用它自己的测试替换它们。 删除 Data 目录和 GlobalSetup.fs、Tests2.fs 和 Tests3.fs 文件。 F# 项目会显式列出其源文件,因此还要将 Compile 中包含 项的 ItemGroup 替换为以下内容:

<ItemGroup>
  <Compile Include="Tests.fs" />
</ItemGroup>

现在,将 MathService 类库添加为项目的另一个依赖项。 使用 dotnet reference add 命令:

dotnet reference add ../MathService/MathService.fsproj

以下大纲显示了最终解决方案布局:

/unit-testing-with-fsharp
    unit-testing-with-fsharp.slnx
    /MathService
        Source Files
        MathService.fsproj
    /MathService.Tests
        Test Source Files
        MathService.Tests.fsproj

在 unit-testing-with-fsharp 目录中执行以下命令:

dotnet sln add ./MathService.Tests/MathService.Tests.fsproj

配置 dotnet 测试的 MTP 模式

由于 TUnit 不支持 VSTest, dotnet test 因此必须在其 MTP 模式下运行。 在 unit-testing-with-fsharp 目录中,创建包含以下内容的 global.json 文件:

{
    "test": {
        "runner": "Microsoft.Testing.Platform"
    }
}

有关详细信息,请参阅 dotnet 测试的 MTP 模式。

创建第一个测试

编写一个失败测试,使其通过,然后重复此过程。 将 Tests.fs 的内容替换为以下代码:

namespace MathService.Tests

open TUnit.Core
open TUnit.Assertions
open TUnit.Assertions.Extensions
open TUnit.Assertions.FSharp.TaskAssert
open MathService

type TestClass () =

    [<Test>]
    member this.TestMethodPassing() = taskAssert {
        do! Assert.That(true).IsTrue()
    }

    [<Test>]
    member this.FailEveryTime() = taskAssert {
        do! Assert.That(false).IsTrue()
    }

该 [<Test>] 属性表示由测试运行程序运行的测试方法。 TUnit 发现类中的测试方法;它不会发现在模块中用 let 声明的函数。 TUnit 断言是异步的。 来自 TUnit.Assertions.FSharp 的计算表达式 taskAssert 将测试作为任务运行,并允许你使用 do! 等待每个断言。

在 unit-testing-with-fsharp 目录中,执行 dotnet test 以生成测试和类库,然后运行测试。 TUnit 测试项目包含用于运行测试的程序入口点。 dotnet test 使用已创建的单元测试项目启动测试应用程序。

这两个测试展示了最基本的通过和失败情况。 TestMethodPassing 通过,而 FailEveryTime 失败。 现在,为 squaresOfOdds 该方法创建一个测试。 该方法 squaresOfOdds 返回属于输入序列的所有奇数整数值的平方序列。 可以迭代创建验证功能的测试,而不是一次性编写所有这些函数。 若要使每个测试通过,请为该方法创建必要的功能。

可以写入的最简单测试是使用所有偶数进行调用 squaresOfOdds ,其中结果应为空整数序列。 将以下测试添加到 TestClass 类型:

[<Test>]
member this.TestEvenSequence() = taskAssert {
    let expected = Seq.empty<int>
    let actual = MyMath.squaresOfOdds [2; 4; 6; 8; 10]
    do! Assert.That<int>(actual).IsEquivalentTo(expected)
}

F# 在解析方法重载时比 C# 更严格,因此测试会将项类型传递给 Assert.That<int>,以选择整数序列的断言。 IsEquivalentTo 检查这两个序列是否包含相同的项。

运行测试时,会看到测试失败。 因为尚未创建实现。 通过在 MathService 项目中的 Library.fs 文件中编写最简单的代码来使此测试通过,该代码有效:

let squaresOfOdds xs =
    Seq.empty<int>

在 unit-testing-with-fsharp 目录中,再次运行 dotnet test 。 dotnet test 命令构建 MathService 项目,然后构建 MathService.Tests 项目。 构建这两个项目后,它将运行你的测试。 现在两个测试通过。

完成所有要求

你已经通过了一个测试,现在可以编写更多测试。 下一个简单的事例适用于唯一奇数为 1的序列。 数字 1 更容易,因为 1 的正方形为 1。 下面是下一个测试:

[<Test>]
member public this.TestOnesAndEvens() = taskAssert {
    let expected = [1; 1; 1; 1]
    let actual = MyMath.squaresOfOdds [2; 1; 4; 1; 6; 1; 8; 1; 10]
    do! Assert.That<int>(actual).IsEquivalentTo(expected)
}

执行 dotnet test 时,新测试失败。 必须更新 squaresOfOdds 该方法才能处理此新测试。 必须筛选序列中的所有偶数,以使此测试通过。 为此,可以编写一个小型筛选器函数并使用 Seq.filter:

let private isOdd x = x % 2 <> 0

let squaresOfOdds xs =
    xs
    |> Seq.filter isOdd

还要执行一个步骤:计算每个奇数的平方值。 首先编写新的测试:

[<Test>]
member public this.TestSquaresOfOdds() = taskAssert {
    let expected = [1; 9; 25; 49; 81]
    let actual = MyMath.squaresOfOdds [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
    do! Assert.That<int>(actual).IsEquivalentTo(expected)
}

可以通过映射操作传递经过筛选的序列来计算每个奇数的平方,以此方式来修复测试的缺陷:

let private square x = x * x
let private isOdd x = x % 2 <> 0

let squaresOfOdds xs =
    xs
    |> Seq.filter isOdd
    |> Seq.map square

你已为该库生成了一个小库和一组单元测试。 你构建了解决方案,以便添加新的包和测试是正常工作流的一部分。 你大部分时间都专注于解决应用程序的目标。

另请参阅