zoukankan      html  css  js  c++  java
  • 使用FsCheck编写Property-based测试

    使用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类型。

  • 相关阅读:
    Dictionaries and lists
    Looping and dictionaries
    Dictionary as a set of counters
    Dictionaries
    List exercise
    Lists and strings
    Copying lists
    Objects and values
    [luoguP1944] 最长括号匹配_NOI导刊2009提高(1)
    [luoguP1868] 饥饿的奶牛(DP)
  • 原文地址:https://www.cnblogs.com/xiandnc/p/10501882.html
Copyright © 2011-2022 走看看