我们如何在调用测试方法之前静态初始化测试数据?
How do we statically initialize test data before calling a test method?
我们有以下测试设置。
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 : Test data returned null for Permute2Tests.removeFirst. Make sure it is statically initialized before this test method is called.
奇怪的是,Permute1Tests.fs
运行没有错误。它的测试通过了。而且,如果我们将 ItemGroup
中的 Permute1Test.fs
位置与 Permute2Test.fs
交换,那么后者现在可以工作,而前者有错误。
我们如何在调用测试方法之前静态初始化测试数据?似乎 ItemGroup
顺序在我们当前的方法中很重要,这使我们当前的方法失败了。
以上代码完整版is here.
编辑:ILSpy 输出
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>
为什么
这与以下事实有关:您的程序集是 "executable"(即具有入口点的程序)而不是 "library"。
F# 编译可执行文件与库略有不同:在入口点所在的模块中,所有静态数据都在 main
函数内部初始化,然后再执行其他所有内容;在所有其他模块中,静态数据在静态构造函数中初始化。我不确定这个决定背后的原因是什么,但这就是 F# 编译器的行为方式。
接下来,F# 编译器如何确定哪个模块包含入口点?很简单:最后一个模块就是入口点。想一想,这是唯一明智的选择:因为 F# 有编译顺序,所以只有最后一个文件可以访问所有其他文件中的定义;因此,这就是入口点所在的位置。
因此,在您的示例中,列表中最后一个模块以 main
函数结束,静态初始化代码位于该函数中。由于单元测试 运行ner 在执行测试之前没有 运行 入口点,因此该模块中的静态数据仍未初始化。
解决方案 1:添加人工模块以包含入口点
正如您自己发现的那样,一种解决方案是添加一个只包含入口点的人工模块。这样,测试模块将不再是最后一个,将不再包含入口点,因此它的数据将在静态构造函数中初始化。
人造模块甚至不必有[<EntryPoint>] main
功能,它可以是这样的:
module Dummy
let _x = 0 // `do ()` would be even shorter, but that will create a warning
编译器无论如何都会添加一个入口点。
解决方案 2:编译为 netstandard2.0
如果您将目标从 netcoreapp2.0
切换到 netstandard2.0
,您的程序集将被视为 "library" 而不是 "executable",并且编译器不会添加一个入口点,并且不会在其中放置静态初始化。
我们有以下测试设置。
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 : Test data returned null for Permute2Tests.removeFirst. Make sure it is statically initialized before this test method is called.
奇怪的是,Permute1Tests.fs
运行没有错误。它的测试通过了。而且,如果我们将 ItemGroup
中的 Permute1Test.fs
位置与 Permute2Test.fs
交换,那么后者现在可以工作,而前者有错误。
我们如何在调用测试方法之前静态初始化测试数据?似乎 ItemGroup
顺序在我们当前的方法中很重要,这使我们当前的方法失败了。
以上代码完整版is here.
编辑:ILSpy 输出
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>
为什么
这与以下事实有关:您的程序集是 "executable"(即具有入口点的程序)而不是 "library"。
F# 编译可执行文件与库略有不同:在入口点所在的模块中,所有静态数据都在 main
函数内部初始化,然后再执行其他所有内容;在所有其他模块中,静态数据在静态构造函数中初始化。我不确定这个决定背后的原因是什么,但这就是 F# 编译器的行为方式。
接下来,F# 编译器如何确定哪个模块包含入口点?很简单:最后一个模块就是入口点。想一想,这是唯一明智的选择:因为 F# 有编译顺序,所以只有最后一个文件可以访问所有其他文件中的定义;因此,这就是入口点所在的位置。
因此,在您的示例中,列表中最后一个模块以 main
函数结束,静态初始化代码位于该函数中。由于单元测试 运行ner 在执行测试之前没有 运行 入口点,因此该模块中的静态数据仍未初始化。
解决方案 1:添加人工模块以包含入口点
正如您自己发现的那样,一种解决方案是添加一个只包含入口点的人工模块。这样,测试模块将不再是最后一个,将不再包含入口点,因此它的数据将在静态构造函数中初始化。
人造模块甚至不必有[<EntryPoint>] main
功能,它可以是这样的:
module Dummy
let _x = 0 // `do ()` would be even shorter, but that will create a warning
编译器无论如何都会添加一个入口点。
解决方案 2:编译为 netstandard2.0
如果您将目标从 netcoreapp2.0
切换到 netstandard2.0
,您的程序集将被视为 "library" 而不是 "executable",并且编译器不会添加一个入口点,并且不会在其中放置静态初始化。