使用FsCheck編寫Property-based測試

使用FsCheck編寫Property-based的測試

編寫基於Property-based的單元測試一文中,咱們介紹了什麼是Property-based測試。同時咱們也總結了Property-based測試的兩個策略:html

  • 隨機產生若干個輸入值,保證足夠多的測試用例
  • 斷言被測代碼具備廣泛適應性的屬性

FsCheck是一個F#版本的QuickCheck移植版本,本文將介紹如何使用FsCheck。java

初識FsCheck

FsCheck是一個用來編寫Property-based測試的工具,開發者經過總結和概括代碼知足的屬性(Properties),利用FsCheck生成大量隨機的輸入對總結的屬性進行驗證。FsCheck提供了一系列方式讓你組合各種屬性,同時還提供了各類數據類型的生成器。
新建一個Console應用程序,添加Nuget:git

Install-Package FsCheck -Version 2.13.0

編寫一個簡單的測試case:github

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

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類型。

相關文章
相關標籤/搜索