首页 > 代码库 > 11.2.1.2 在 F# 中写单元测试

11.2.1.2 在 F# 中写单元测试

11.2.1.2 在 F# 中写单元测试

 

如果我们以这种方式写直接测试的代码,很容易把它改成单元测试,成为大项目的一个部分。很快,我们将讨论如何用xUnit.net 来实现,但现在,我们要写另一个应由单元测试明确覆盖的调用:用null 值作为参数值,调用getLongest 函数:

 

> getLongest(null);;

Program.fs(24,12): error FS0043: The type ‘stringlist‘

does not have ‘null‘ as a proper value

 

这个,我们在之前还没尝试过,可以看到,F# 交互控制台报告一个编译时错误,而不是异常,因此,我们甚至不可能写出这样的代码,这是说,如果我们只在F# 中使用这个函数,根本不需要测试。在F# 中声明的类型值(包括差别联合,记录,以及F# 的类声明),根本不允许null 值,它们必须初始化为有效的值。在第五章我们已经知道,表示F# 中的缺失值的正确方式是使用option 类型,但是,这个规则只在F# 中使用,且用于声明F# 中的类型。当调用通常的.NET 方法,用已有的.NET 类型作为参数,是可以指定null 作为有效的参数值的。

 

注意

 

其他语言,比如C#,不理解F# 类型不允许null 值的限制。因此,F# 函数,比如getLongest,如果从C# 中调用,仍然能够可以接收null 作为参数值。我们可以在函数内检查这种情况,通过使用泛型值Unchecked.defaultof<‘T>,这是一种不安全的方法,用于创建 F# 中任何引用类型的 null值,或者获得值类型的默认值;换句话说,它相当于C# 中的default(T)。另外,这种技巧还可以用于写单元测试,来验证函数的行为。这并不经常需要,因为F# 库的公共API 倾向于使用标准的.NET 类型,比如seq <‘T>,它把null 作为有效值,因此,我们可以按常规方式用这个API 写单元测试。

 

我们只打算在F# 中使用这个简单的函数,因此,不必考虑C# 用户用null 作为参数值调用的情况。清单11.8 显示了我们添加的几个其他测试。注意,清单的很大一部分代码是对清单11.7 的版本,以交互方式测试函数的稍许修改。最明显的区别是,我们已经把测试代码打包到函数内,然后添加一个特性,标记为xUnit.net 测试。

 

Listing 11.8 用单元测试验证函数的行为 (F#)

#if INTERACTIVE

#r @"C:\Programs\Development\xUnit\xunit.dll"

#endif

open Xunit

 

let getLongest(names:list<string>) =

  names|> List.maxBy (fun name -> name.Length)

C Requires first of the

longest elements

module LongestTests =

  [<Fact>]  <-- 用特性标记测试

  letlongestOfNonEmpty() =

    lettest = [ "Aaa"; "Bbbbb"; "Cccc" ]      | 调整后的交互式测试

    Assert.Equal("Bbbbb",getLongest(test))  |

 

  [<Fact>]

  letlongestFirstLongest() =

    lettest = [ "Aaa"; "Bbb" ]            | [2]

    Assert.Equal("Aaa",getLongest(test))  | 期望第一个最长元素

 

  [<Fact>]

    letlongestOfEmpty() =

    lettest = []                    | [3]

    Assert.Equal("",getLongest(test))  | 对于空列表,期望空空字符串

 

除了把每个测试打包成一个函数以外,我们还创建一个模块,以保持所有单元都测试在一个类当中,从技术上讲,这不是必须的,但却是一个好主意,它能使测试与程序的主体分离。根据自己的喜好,可以将测试移动到文件的末尾,或者是项目中的单独文件,甚至是一个单独的项目。

XUnit.net 框架使用 Fact 特性值来标记方法,表示单元测试[1]。我们可以将此应用到有let 绑定的F# 函数声明上,因为它会编译成为方法。模块中的第一个测试,是我们写的以交互方式测试代码调整后的版本,我们还添加两个新的测试。

第二个测试[2]验证getLongest 函数,当有几个元素时,返回具有最大长度元素中的第一个。F# 库中的MaxBy 函数符合这条规则,但它不归档,可能取决于具体的实现,因此,显式地对其进行测试,是个好主意。最后的测试[3] 检查当我们传递空列表给函数时,是否返回空字符串,这是值得考虑的边界情况。例如,当在用户界面中显示结果时,返回空字符串可能是期望的行为。你可能已经猜到,我们原来的执行并不符合这个规则。如果在已编译的程序集中运行xUnit.net 图形用户界面,将得到类似于图11.1 的结果。

 

图11.1 当测试函数的参数值是空列表时,会引发异常,而不是返回空字符串。

 

现在,我们已经阐明了getLongest 函数的期望行为,就可以通过添加一个模式,匹配空列表,很容易改正:

 

let getLongest(names:list<string>) =

  matchnames with

  | []–> "" 

  | _-> names |> List.maxBy (fun name -> name.Length)

 

经过后这样的修改,所有三个单元测试都通过了。到目前为止,所有测试都还相当简单,我们只要检查返回的字符串是否与预期的相匹配。通常,单元测试是比这个更棘手。我们现在要看看测试更复杂的函数,尤其是当函数返回列表时,如何比较实际值与期望值。

11.2.1.2 在 F# 中写单元测试