我们进行了以下测试设置。
Permutations.Tests.fsproj
<ItemGroup>
<Compile Include="Permute1Tests.fs" />
<Compile Include="Permute2Tests.fs" />
</ItemGroup>
Permute1Tests.fs
module Permute1Tests
open Xunit
open Permutations.Permute1
[<Theory>]
[<MemberData("permuteTestValues")>]
let ``permute`` (x, expected) =
let actual = permute x
Assert.Equal<List<int>>(expected, actual);
let permuteTestValues : obj array seq =
seq {
yield [| [0;1]; [[0;1]; [1;0]] |]
}
Permute2Tests.fs
module Permute2Tests
open Xunit
open Permutations.Permute2
[<Theory>]
[<MemberData("removeFirstTestData")>]
let ``removeFirst`` (item, list, expected: List<int>) =
let actual = removeFirst list item
Assert.Equal<List<int>>(expected, actual)
let removeFirstTestData : obj array seq =
seq {
yield [| 0; [1;2;3;4]; [1;2;3;4] |]
}
当我们运行dotnet test
时,这就是错误:
System.InvalidOperationException:Permute2Tests.removeFirst为测试数据返回null。在调用此测试方法之前,请确保它已静态初始化。
奇怪的是,Permute1Tests.fs
运行没有错误。它的测试通过。而且,如果我们将Permute1Test.fs
中的ItemGroup
位置与Permute2Test.fs
进行交换,则后者现在可以正常工作而前者有错误。
在调用测试方法之前,我们如何静态初始化测试数据?似乎ItemGroup
顺序在我们当前的方法中很重要,这使得我们当前的方法失败了。
上述代码的完整版is here。
Permute1Tests.fs.cs
// <StartupCode$Permutations-Tests>.$Permute1Tests
using <StartupCode$Permutations-Tests>;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
internal static class $Permute1Tests
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal static readonly IEnumerable<object[]> permuteTestValues@12;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[CompilerGenerated]
[DebuggerNonUserCode]
internal static int init@;
static $Permute1Tests()
{
IEnumerable<object[]> permuteTestValues =
$Permute1Tests.permuteTestValues@12 =
(IEnumerable<object[]>)new Permute1Tests.permuteTestValues@14(0, null);
}
}
Permute2Tests.fs.cs
// <StartupCode$Permutations-Tests>.$Permute2Tests
using <StartupCode$Permutations-Tests>;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
internal static class $Permute2Tests
{
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
internal static IEnumerable<object[]> removeFirstTestData@15;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
[CompilerGenerated]
[DebuggerNonUserCode]
internal static int init@;
public static void main@()
{
IEnumerable<object[]> removeFirstTestData =
$Permute2Tests.removeFirstTestData@15 =
(IEnumerable<object[]>)new Permute2Tests.removeFirstTestData@17(0, null);
}
}
Permutations.Test.fsproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<Compile Include="Permute1Tests.fs" />
<Compile Include="Permute2Tests.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
<DotNetCliToolReference Include="dotnet-xunit" Version="2.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Permutations\Permutations.fsproj" />
</ItemGroup>
</Project>
答案 0 :(得分:3)
这与您的程序集是“可执行文件”(即具有入口点的程序)而不是“库”这一事实有关。
F#编译可执行文件的方式与库略有不同:在入口点所在的模块中,所有静态数据都在main
函数内初始化,然后再执行其他操作;在所有其他模块中,静态数据在静态构造函数中初始化。我不确定这个决定背后的原因是什么,但这就是F#编译器的行为方式。
接下来,F#编译器如何确定哪个模块包含入口点?非常简单:无论哪一个模块是最后一个模块,这就是入口点所在的位置。想一想,这是唯一明智的选择:因为F#有编译顺序,所以只有最后一个文件才能访问所有其他文件中的定义;因此,这就是入口点必须的地方。
因此,在您的示例中,无论哪个模块位于列表的最后一个模块,最后都会使用main
函数,其中包含静态初始化代码。由于单元测试运行器在执行测试之前没有运行入口点,因此该模块中的静态数据仍然未初始化。
正如您已经发现的那样,一种解决方案是添加一个只包含入口点的人工模块。这样,测试模块将不再是最后一个,不包含入口点,因此它的数据将在静态构造函数中初始化。
人工模块甚至不需要[<EntryPoint>] main
功能,它可能就是这样:
module Dummy
let _x = 0 // `do ()` would be even shorter, but that will create a warning
编译器无论如何都会添加一个入口点。
netstandard2.0
如果您将目标从netcoreapp2.0
切换到netstandard2.0
,您的程序集将被视为“库”而不是“可执行文件”,并且编译器不会添加入口点,并且赢了不要在其中加入静态初始化。