使用FsCheck编写Property-based的测试

编写基于Property-based的单元测试一文中,我们介绍了什么是Property-based测试。同时我们也总结了Property-based测试的两个策略:

  • 随机产生若干个输入值,保证足够多的测试用例
  • 断言被测代码具有普遍适应性的属性

FsCheck是一个F#版本的QuickCheck移植版本,本文将介绍如何使用FsCheck。

初识FsCheck

FsCheck是一个用来编写Property-based测试的工具,开发者通过总结和归纳代码满足的属性(Properties),利用FsCheck生成大量随机的输入对总结的属性进行验证。FsCheck提供了一系列方式让你组合各类属性,同时还提供了各种数据类型的生成器。

新建一个Console应用程序,添加Nuget:

Install-Package FsCheck -Version 2.13.0

编写一个简单的测试case:

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里会输出下面的内容:

Number always not equal self add 1-Ok, passed 100 tests.

FsCheck随机产生了100个输入,并且这个100个测试都通过了, 我们稍微修改下上面的代码,将QuickCheck方法改为VerboseCheck,让Console输出更加详细的日志:

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类型,用户生成T类型的随机数据,下面是一个最基本的例子,创建了一个Constant类型的Generator,通过Gen.Sample方法创建5个字符串:

var gen = Gen.Constant("foo");
var strings = Gen.Sample(10, 5, gen);

F#:

let constantStrings = Gen.constant("Foo") |> Gen.sample 0 5

Constant类型的Genrator算是最简单的Genrator,用于生成同一个值,在上面的例子中,将会生成具有5个元素的list:

["foo"; "foo"; "foo"; "foo"; "foo"]

因为FsCheck的功能是生成随机数据,所以Constant类型的Genrator其实不太常用,但是非常便于理解Genrator的作用。

Choose

var gen = Gen.Choose(1, 10);
var value = Gen.Sample(0, 5, gen);

F#:

 Gen.choose(1, 10) |> Gen.sample 0 5

choose函数接受一个最小值和最大值,创建的Generator在最小值和最大值之间生成数字,上面的例子中分别为1和10。Gen.Sample函数接受三个参数,用于使用某一个Generator创建一组样本,第一个参数0在本例中不起作用,5表示生成5个值,生成的数据为:

[2; 7; 10; 7; 7]

Elements

Gen.Elements用于从一组元素列表中创建一个Generator,从提供的元素列表中选取随机值,例如下面的实例:

var gen = Gen.Elements(42, 1337, 7, -100, 1453, -273);
var value = Gen.Sample(0, 5, gen);

F#:

Gen.elements [42; 1337; 7; -100; 1453; -273] |> Gen.sample 0 10

在上面的例子中,先定义了5个元素,Generator会随机从这5个元素中选取值,最后生成的结果如下:

[1453; 1337; 7; -273; 42; -100; 1453; 1337; 7; -273]

GrowingElements

Gen.GrowingElements跟Gen.Elements特别像,只有一个区别, Gen.Sample函数的第一个参数会起作用,例如定义下面的元素列表:

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#:

 Gen.growingElements ['a'; 'b'; 'c'; 'd'; 'e'; 'f'; 'g'; 'h'; 'i'; 'j']
|> Gen.sample 3 10

Gen.Sample(3, 5, gen)意味着要生成5个值,并且每个值只能在第三个(‘c'),包括第三个之内,生成的数据如下:

['a'; 'a'; 'b'; 'b'; 'c'; 'c'; 'a'; 'a'; 'a'; 'c']

Map

Gen.Map是一个投影函数,正如List类型一样,Gen类型也可以直接通过Select方法将T类型映射成别的类型。

 var gen = Gen.Choose(1, 30)
.Select(x => new DateTime(2019, 11, x).ToString("u"));
var value = Gen.Sample(0, 10, gen);

F#:

Gen.choose (1, 3) |> Gen.map (fun i -> DateTime(2019, 11, i).ToString "u")
|> Gen.sample 0 10

上面的例子先通过Gen.choose生成1到30日期的随机数,然后在映射为日期。生成的数据如下:

["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。

var gen = Gen.Constant(42).ListOf(1);
var value = Gen.Sample(0, 10, gen);

F#

Gen.constant 42 |> Gen.listOf |> Gen.sample 1 10

上面的例子使用Gen.Constant 42来作为每一个List元素的Generator,通过这种方式生成的lists只包含42。生成的数据如下:

[[42]; [42]; [42]; [42]; [42]; [42]; [42]; [42]; [42]; [42]]

除了使用Gen.ListOf,你还可以通过Gen.NoEmptyListOf来生成至少包含有一个元素的lists:

var gen = Gen.Elements("foo", "bar", "baz")
.NonEmptyListOf();
var value = Gen.Sample(3, 4, gen);

F#

Gen.elements ["foo"; "bar"; "baz"] |> Gen.nonEmptyListOf |> Gen.sample 3 4

生成的输入如下:

[["foo"; "bar"; "baz"], ["foo"], ["baz", "bar"]]

Filter

你已经可以通过上面的Generator来生成各种各样的数据了,你还可以通过Gen.Filter进行过滤,例如下面的例子:

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#

Gen.choose (1, 100)
|> Gen.two
|> Gen.filter (fun (x, y) -> x <> y)
|> Gen.map (fun (x, y) -> [x; y])
|> Gen.sample 0 10

生成的数据如下:

[[30; 89]; [12; 82]; [66; 47]; [82; 40]; [64; 5]; [18; 35]; [61; 42]; [14; 29];
[83; 93]; [100; 37]]

掌握了Generator,下一篇将介绍Shrinker和自定义Arbitrary类型。

使用FsCheck编写Property-based测试的更多相关文章

  1. 20162311 编写Android程序测试查找排序算法

    20162311 编写Android程序测试查找排序算法 一.设置图形界面 因为是测试查找和排序算法,所以先要有一个目标数组.为了得到一个目标数组,我设置一个EditText和一个Button来添加数 ...

  2. Rspec: everyday-rspec实操: 第9章 快速编写测试,编写快速的测试。

    Make it work, make it right, make it fast. 测试运行的时间.应用和测试组件的增长,速度会越来越慢,目标是保持代码的readable, maintainable ...

  3. [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being

    [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent c ...

  4. vue报错 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's

    [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent c ...

  5. 【cypress】3. 编写第一个测试

    当环境安装好了之后,就可以着手尝试第一个测试的编写了. 一.新建一个文件 在你的项目下的cypress/integration文件夹中创建一个新文件sample_spec.js,我这里直接在webst ...

  6. 在Jmeter中使用自定义编写的Java测试代码

    我们在做性能测试时,有时需要自己编写测试脚本,很多测试工具都支持自定义编写测试脚本,比如LoadRunner就有很多自定义脚本的协议,比如"C Vuser","Java ...

  7. Appium之编写H5应用测试脚本(切换到Webview)

    App使用H5编写,默认方式找不到元素.启动后获取所有上下文,找到webivew_xxxx的,然后进行切换. 源码: package MyAppium; import io.appium.java_c ...

  8. 阶段3 3.SpringMVC·_07.SSM整合案例_07.ssm整合之编写MyBatis框架测试保存的方法

    再写一个测试的方法,测试save保存的方法 需要提交事务才能保存到数据库

  9. WCF编写时候的测试

    1右击WCF创建到使用到发布这篇文章中的类库项目中的接口类实现文件添加断点 2右击WCF创建到使用到发布这篇文章中的WCF服务网站设为启动项并允许 3右击WCF创建到使用到发布这篇文章中的WPF项目调 ...

随机推荐

  1. B20J_2243_[SDOI2011]染色_树链剖分+线段树

    B20J_2243_[SDOI2011]染色_树链剖分+线段树 一下午净调这题了,争取晚上多做几道. 题意: 给定一棵有n个节点的无根树和m个操作,操作有2类: 1.将节点a到节点b路径上所有点都染成 ...

  2. Python+Appium 获取 toast 文本值方法的封装

    获取toast内容方法封装如下: def get_Toast(self,message): #查找toast值 ''' method explain:查找toast的值,与find_Toast实现方法 ...

  3. appium 出现报错“A new session could not be created. (Original error: Requested a new session but one was in progress)”的解决方式!

    报错点:selenium.common.exceptions.WebDriverException: Message: A new session could not be created. (Ori ...

  4. Java动态代理之JDK实现和CGlib实现

    一:代理模式(静态代理) 代理模式是常用设计模式的一种,我们在软件设计时常用的代理一般是指静态代理,也就是在代码中显式指定的代理. 静态代理由 业务实现类.业务代理类 两部分组成.业务实现类 负责实现 ...

  5. 浅谈.Net异步编程的前世今生----APM篇

    前言 在.Net程序开发过程中,我们经常会遇到如下场景: 编写WinForm程序客户端,需要查询数据库获取数据,于是我们根据需求写好了代码后,点击查询,发现界面卡死,无法响应.经过调试,发现查询数据库 ...

  6. SpringCloud-服务注册与发现(注册中心)

    SpringCloud-服务注册与发现(注册中心) 作者 : Stanley 罗昊 [转载请注明出处和署名,谢谢!] 注:作者使用IDEA + Gradle 注:需要有一定的java&& ...

  7. 5.3Role和Claims授权「深入浅出ASP.NET Core系列」

    希望给你3-5分钟的碎片化学习,可能是坐地铁.等公交,积少成多,水滴石穿,码字辛苦,如果你吃了蛋觉得味道不错,希望点个赞,谢谢关注. Role授权 这是一种Asp.Net常用的传统的授权方法,当我们在 ...

  8. cocos creator主程入门教程(四)—— 网络通信

    五邑隐侠,本名关健昌,10年游戏生涯,现隐居五邑.本系列文章以TypeScript为介绍语言. 前面已经介绍怎样加载资源.管理弹窗.开发一个网络游戏,难免要处理网络通信.有几点问题需要注意: 1.服务 ...

  9. ssm(Spring、Springmvc、Mybatis)实战之淘淘商城-第一天

    文章大纲 一.课程介绍二.淘淘商城基本介绍三.后台管理系统工程结构与搭建四.svn代码管理五.项目源码与资料下载六.参考文章   一.课程介绍 1. 课程大纲 一共14天课程(1)第一天:电商行业的背 ...

  10. Maven 基本的认识

    Maven 基本的认识 1. 什么是Maven? 在平时开发中,经常遇到某个jar包,我在代码层已经Import 和@Automation了,编译器还是提醒你某个jar包找不到,往往这时来个mvn i ...