使用FsCheck编写Property-based测试
目录
初识FsCheck
利用Generator创建测试数据
使用FsCheck编写Property-based的测试
在编写基于Property-based的单元测试一文中,我们介绍了什么是Property-based测试。同时我们也总结了Property-based测试的两个策略:
- 随机产生若干个输入值,保证足够多的测试用例
- 断言被测代码具有普遍适应性的属性
FsCheck是一个F#版本的QuickCheck移植版本,本文将介绍如何使用FsCheck。
初识FsCheck
FsCheck是一个用来编写Property-based测试的工具,开发者通过总结和归纳代码满足的属性(Properties),利用FsCheck生成大量随机的输入对总结的属性进行验证。FsCheck提供了一系列方式让你组合各类属性,同时还提供了各种数据类型的生成器。
新建一个Console应用程序,添加Nuget:
1 | Install-Package FsCheck -Version 2.13.0 |
编写一个简单的测试case:
1 2 3 4 5 6 7 | static void Main( string [] args) { Prop.ForAll< int >(x => x != x + 1 ) .QuickCheck( "Number always not equal self add 1" ); Console.ReadKey(); } |
我们定义了一个永远都为true的Property: x != x + 1,如果把这个代码跑起来,Console里会输出下面的内容:
1 | Number always not equal self add 1-Ok, passed 100 tests. |
FsCheck随机产生了100个输入,并且这个100个测试都通过了, 我们稍微修改下上面的代码,将QuickCheck方法改为VerboseCheck,让Console输出更加详细的日志:
1 2 3 4 5 6 | static void Main( string [] args) { Prop.ForAll< int >(x => x != x + 1) .VerboseCheck( "Number always not equal self add 1" ); Console.ReadKey(); } |
这次Console会打出100个随机的输入。
利用Generator创建测试数据
FsCheck提供了一套用于创建随机数据的方式,分别为Generator,Shrinker,Arbitrary:
Generator用于创建随机数据,FsCheck已经定义了一些用于生成基本类型的Generator,当然你还可以自定义Generator,用来生成任意类型的随机数据。
Generator是一个Gen
1 2 | var gen = Gen.Constant( "foo" ); var strings = Gen.Sample(10, 5, gen); |
F#:
1 | let constantStrings = Gen.constant( "Foo" ) |> Gen.sample 0 5 |
Constant类型的Genrator算是最简单的Genrator,用于生成同一个值,在上面的例子中,将会生成具有5个元素的list:
1 | [ "foo" ; "foo" ; "foo" ; "foo" ; "foo" ] |
因为FsCheck的功能是生成随机数据,所以Constant类型的Genrator其实不太常用,但是非常便于理解Genrator的作用。
Choose
1 2 | var gen = Gen.Choose(1, 10); var value = Gen.Sample(0, 5, gen); |
F#:
1 | Gen.choose(1, 10) |> Gen.sample 0 5 |
choose函数接受一个最小值和最大值,创建的Generator在最小值和最大值之间生成数字,上面的例子中分别为1和10。Gen.Sample函数接受三个参数,用于使用某一个Generator创建一组样本,第一个参数0在本例中不起作用,5表示生成5个值,生成的数据为:
1 | [2; 7; 10; 7; 7] |
Elements
Gen.Elements用于从一组元素列表中创建一个Generator,从提供的元素列表中选取随机值,例如下面的实例:
1 2 | var gen = Gen.Elements(42, 1337, 7, -100, 1453, -273); var value = Gen.Sample(0, 5, gen); |
F#:
1 | Gen.elements [42; 1337; 7; -100; 1453; -273] |> Gen.sample 0 10 |
在上面的例子中,先定义了5个元素,Generator会随机从这5个元素中选取值,最后生成的结果如下:
1 | [1453; 1337; 7; -273; 42; -100; 1453; 1337; 7; -273] |
GrowingElements
Gen.GrowingElements跟Gen.Elements特别像,只有一个区别, Gen.Sample函数的第一个参数会起作用,例如定义下面的元素列表:
1 2 3 | var gen = Gen.GrowingElements( new List< char > { 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , 'i' , 'j' }); var value = Gen.Sample(3, 5, gen); |
F#:
1 2 | Gen.growingElements [ 'a' ; 'b' ; 'c' ; 'd' ; 'e' ; 'f' ; 'g' ; 'h' ; 'i' ; 'j' ] |> Gen.sample 3 10 |
Gen.Sample(3, 5, gen)意味着要生成5个值,并且每个值只能在第三个(‘c'),包括第三个之内,生成的数据如下:
1 | [ 'a' ; 'a' ; 'b' ; 'b' ; 'c' ; 'c' ; 'a' ; 'a' ; 'a' ; 'c' ] |
Map
Gen.Map是一个投影函数,正如List
1 2 3 | var gen = Gen.Choose(1, 30) .Select(x => new DateTime(2019, 11, x).ToString( "u" )); var value = Gen.Sample(0, 10, gen); |
F#:
1 2 | Gen.choose (1, 3) |> Gen.map (fun i -> DateTime(2019, 11, i).ToString "u" ) |> Gen.sample 0 10 |
上面的例子先通过Gen.choose生成1到30日期的随机数,然后在映射为日期。生成的数据如下:
1 2 3 4 | [ "2019-11-24 00:00:00Z" ; "2019-11-15 00:00:00Z" ; "2019-11-28 00:00:00Z" ; "2019-11-19 00:00:00Z" ; "2019-11-02 00:00:00Z" ; "2019-11-23 00:00:00Z" ; "2019-11-06 00:00:00Z" ; "2019-11-27 00:00:00Z" ; "2019-11-10 00:00:00Z" ; "2019-11-24 00:00:00Z" ] |
List
除了上面的Generator能够生成一组随机值,你还可以通过Gen.listOf, Gen.ListOfLength, Gen.NonEmptyListOf生成List元素,例如你可以通过Gen.Constant来生成一组包含同一个常量的List。
1 2 | var gen = Gen.Constant(42).ListOf(1); var value = Gen.Sample(0, 10, gen); |
F#
1 | Gen.constant 42 |> Gen.listOf |> Gen.sample 1 10 |
上面的例子使用Gen.Constant 42来作为每一个List元素的Generator,通过这种方式生成的lists只包含42。生成的数据如下:
1 | [[42]; [42]; [42]; [42]; [42]; [42]; [42]; [42]; [42]; [42]] |
除了使用Gen.ListOf,你还可以通过Gen.NoEmptyListOf来生成至少包含有一个元素的lists:
1 2 3 | var gen = Gen.Elements( "foo" , "bar" , "baz" ) .NonEmptyListOf(); var value = Gen.Sample(3, 4, gen); |
F#
1 | Gen.elements [ "foo" ; "bar" ; "baz" ] |> Gen.nonEmptyListOf |> Gen.sample 3 4 |
生成的输入如下:
1 | [[ "foo" ; "bar" ; "baz" ], [ "foo" ], [ "baz" , "bar" ]] |
Filter
你已经可以通过上面的Generator来生成各种各样的数据了,你还可以通过Gen.Filter进行过滤,例如下面的例子:
1 2 3 4 5 | var gen = Gen.Choose(1, 100) .Two() .Where(x => x.Item1 != x.Item2) .Select(x => new List< int > {x.Item1, x.Item2}); var value = gen.Sample(0, 10); |
F#
1 2 3 4 5 | Gen.choose (1, 100) |> Gen.two |> Gen.filter (fun (x, y) -> x <> y) |> Gen.map (fun (x, y) -> [x; y]) |> Gen.sample 0 10 |
生成的数据如下:
1 2 | [[30; 89]; [12; 82]; [66; 47]; [82; 40]; [64; 5]; [18; 35]; [61; 42]; [14; 29]; [83; 93]; [100; 37]] |
掌握了Generator,下一篇将介绍Shrinker和自定义Arbitrary类型。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述