单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确。通常情况下,一个单元测试(用例)用于判断某个特定条件(或场景)下特定函数的行为。如果想对单元测试的好处有更多的了解,可以看一下单元测试实战。
在.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的实现
使用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/
本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2008/11/18/fsharp-adventures-unit-test.html,如需转载请自行联系原作者。