代码改变世界

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

2008-11-18 13:42 Anders Cui 阅读(...) 评论(...) 编辑 收藏

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

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

在F#中, LOPLanguage-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的实现


使用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的相关知识。

(要了解本人所写的其它F#随笔请查看 F#系列随笔索引

参考

《Foundations of F#》 by Robert Pickering
http://code.google.com/p/fsunit/