F#探险之旅(七):在F#中进行单元测试

单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常情况下,一个单元测试(用例)用于判断某个特定条件(或场景)下特定函数的行为。如果想对单元测试的好处有更多的了解,可以看一下单元测试实战。

在.NET社区内,NUnit无疑是最经典的单元测试工具,要了解它的用法,建议看一下园子里的一篇很棒的文章NUnit详细使用方法。本文对此不再赘述。另外MbUnit作为后起之秀,也很值得一试。

在F#中, LOP(Language-Oriented Programming)是它的一个亮点,而FsUnit则是LOP的一个很好的实践。FsUnit使用F#开发,用它编写的测试用例会接近于自然语言 (英语),在其中我们也可以看到F#对函数进行组合的强大威力。

在本文中,我将通过简单的例子分别对NUnit和FsUnit的基本用法进行介绍。假定我们在开发一个类库MyFsLib,其中有一个模块mathHelper,里面有一些关于数学的函数,现在要做的就是测试这些函数。mathHelper的代码如下:

F# Code - mathHelper的签名
#light
module MyFsLib.MathHelper
/// 获取一个浮点数的平方值
val square: float -> float
/// 获取一个浮点数的立方值
val cube: float -> float
/// 判断一个整数是否为偶数
val isEven: int -> bool
/// 判断一个整数是否为奇数
val isOdd: int -> bool
/// 获取不大于指定正整数的质数数组
val generatePrimes: int -> int array

F# Code - mathHelper的实现
#light
module MyFsLib.MathHelper
open System
let pow x y = Math.Pow(x, y)
let square x = pow x 2.0
let cube x = pow x 3.0
let isEven x = x % 2 = 0
let isOdd x = x % 2 = 1
// Eratosthenes筛法
let generatePrimes n =
  match n with
  | _ when n < 2 -> [||]
  | _ ->
    // Init sieve.
    let sieve = [| for i in 0 .. n do yield true |]

    let isPrime index = sieve.[index]

    // Check it.
    let upperBound = Convert.ToInt32(Math.Sqrt((float)n))
    for i = 2 to upperBound do
      if isPrime i then
        for j in [i * 2 .. i .. sieve.Length - 1] do
          sieve.[j] <- false

    let mutable count = 0
    for i = 2 to sieve.Length - 1 do
      if isPrime i then
        count <- count + 1

    let primes = Array.create count 0
    let mutable index = 0
    for i = 2 to sieve.Length - 1 do
      if isPrime i then
        primes.[index] <- i
        index <- index + 1

    primes

使用NUnit进行单元测试

不要害怕,由于F#植根于.NET平台的本性,你会发现这些测试用例代码都是那么眼熟。

需要废话的是,先添加对”nunit.framework.dll”的引用,而且要为测试类添加一个无参构造函数。

F# Code - NUnit tester
#light
namespace NUnitTester
open NUnit.Framework
open MyFsLib
[<TestFixture>]
type TestCases = class
  new() = {}

  [<Test>]
  member this.TestSquare() =
    Assert.AreEqual(0, MathHelper.square(0.0))
    Assert.AreEqual(4, MathHelper.square(2.0))

  [<Test>]
  member this.TestGeneratePrimes() =
    let primesLessThan2 = MathHelper.generatePrimes(1)
    CollectionAssert.IsEmpty(primesLessThan2)

    let primesNotGreaterThan2 = MathHelper.generatePrimes(2)
    CollectionAssert.IsNotEmpty(primesNotGreaterThan2)
    CollectionAssert.Contains(primesNotGreaterThan2, 2)
    Assert.AreEqual(1, primesNotGreaterThan2.Length)

    let primesNotGreaterThan10 = MathHelper.generatePrimes(10)
    CollectionAssert.IsNotEmpty(primesNotGreaterThan10)
    CollectionAssert.Contains(primesNotGreaterThan10, 7)
    Assert.AreEqual(4, primesNotGreaterThan10.Length)

  // Other testcases
end

这里只编写了对函数square和generatePrimes的测试。如果你在C#中用过NUnit,看这样的代码就没有任何问题了。测试结果为:



使用FsUnit进行单元测试

FsUnit是一个Specification测试框架。它的目标是将单元测试和行为(函数)的规格尽量简化,并以函数式的风格代替命令式风格的测试代码。

Specification可以翻译为规格说明,就是说测试代码实际上是对待测代码的一条条规格说明。比如对函数square,它求一个数的平方,那么一条规格可以是:”square(2) should equal 4”。好了,惊喜就要来了:

F# Code - FsUnit tester
#light
open FsUnit
open MyFsLib
let squareSpecs =
  specs "Test square" [
    spec "square(0) should equal 0"
      (MathHelper.square(0.0) |> should equal 0.0) // Pass
    spec "square(2) should equal 2"
      (MathHelper.square(2.0) |> should equal 2.0) // Fail
  ]

let generatePrimes1Specs =
  specs "Test generatePrimes" [
    spec "generatePrimes(1).Length should equal 0"
      (MathHelper.generatePrimes(1).Length |> should equal 0)
  ]

let generatePrimes2Specs =
  specs "Test generatePrimes" [
    spec "generatePrimes(2).Length should equal 1"
      (MathHelper.generatePrimes(2).Length |> should equal 1)
    spec "generatePrimes(2) should contain 2"
      (MathHelper.generatePrimes(2) |> should contain 2)
  ]

let generatePrimes10Specs =
  specs "Test generatePrimes" [
    spec "generatePrimes(10).Length should equal 4"
      (MathHelper.generatePrimes(10).Length |> should equal 4)
    spec "generatePrimes(10) should contain 7"
      (MathHelper.generatePrimes(10) |> should contain 7)
    spec "generatePrimes(10) should not contain 9"
      (MathHelper.generatePrimes(10) |> should not' (contain 9))
  ]

printfn "%s" (Results.summary())

这里没有Assert,有的是should,这两个词给人的感觉可大不一样,而且通过函数的组合,我们可以写出should equal这样的“句子”。这里的测试代码已经比较接近自然语言了。

一条spec就是一条规格说明,它说明待测的函数具有什么样的规格,我们把这些都放在specs中,测试结束后,使用Results.summary函数来显示测试结果:

如果对FsUnit感兴趣,可以到http://code.google.com/p/fsunit/这里来看看。感觉目前它还有些欠缺,比如没有像CollectionAssert这样的测试类,接下来看看能不能扩展一下。

小结

本文介绍了在F#中如何使用NUnit和FsUnit进行单元测试。可以看到两者都很简单,前者简单是因为能很好地延续在C#中的方式,迁移过来不 要费多大力气;后者简单是因为它接近自然语言,看起来很亲切。FsUnit值得关注,除了单元测试本身,我们还可以通过它来了解Language- Oriented Programming的相关知识。

你可能感兴趣的:(单元测试)