如何使用FsCheck生成随机数作为基于属性的测试的输入 [英] How to use FsCheck to generate random numbers as input for property-based testing

查看:113
本文介绍了如何使用FsCheck生成随机数作为基于属性的测试的输入的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我认为该是尝试FsCheck的时候了,但事实证明它比我想象的要难.关于Arb,生成器等的文档很多,但是对于如何应用这些知识似乎没有任何指导.或者我只是不明白.

可能更难掌握的是测试,属性,生成器,任意项,收缩与缩小之间的关系,就我而言,随机性(某些测试会自动生成随机数据,而另一些则不会)对我来说并不明确.我没有Haskell背景,因此也无济于事.

现在的问题是:如何生成随机整数?

我的测试场景可以用乘法的性质来解释,比如说分布性:

static member  ``Multiplication is distributive`` (x: int64) y z =
    let res1 = x * (y + z)
    let res2 = x * y + x * z

    res1 = res2

// run it:
[<Test>]
static member FsCheckAsUnitTest() =
    Check.One({ Config.VerboseThrowOnFailure with MaxTest = 1000 }, ``Multiplication is distributive``)

当我使用Check.Verbose或NUnit集成运行此程序时,我得到如下测试序列:

0:
(-1L, -1L, -1L)
1:
(-1L, -1L, 0L)
2:
(-1L, -1L, -1L)
3:
(-1L, -1L, -1L)
4:
(-1L, 0L, -1L)
5:
(1L, 0L, 2L)
6:
(-2L, 0L, -1L)
7:
(-2L, -1L, -1L)
8:
(1L, 1L, -2L)
9:
(-2L, 2L, -2L)

经过1000次测试后,它还没有超过100L.我以某种方式认为这将自动"选择均匀分布在int64整个范围内的随机数,至少这就是我解释文档的方式.

由于没有,所以我开始进行实验,并提出了如下类似的愚蠢方法来获得更高的数字:

type Generators = 
    static member arbMyRecord =
        Arb.generate<int64>
        |> Gen.where ((<) 1000L)
        |> Gen.three
        |> Arb.fromGen

但是,这变得异常缓慢,并且显然不是正确的方法.我敢肯定我必须缺少一个简单的解决方案.我尝试使用Gen.choose(Int64.MinValue, Int64.MaxValue),但这仅支持int,而不支持longs(但即使仅使用int,我也无法使其正常工作).

最后,我需要一个适用于所有原始数值数据类型的解决方案,其中包括它们的最大值和最小值,零和一,以及从内部任何内容中进行的随机选择.

解决方案

其他FsCheck问题所述,大多数Check功能的默认配置为EndSize = 100.您可以增加该数字,但也可以按照建议使用Gen.choose.

即使如此,int生成器也是故意表现得很好.例如,它不包含Int32.MinValueInt32.MaxValue,因为这可能导致溢出.

FsCheck确实提供了生成器,这些生成器为您提供了整个范围内的均匀分布:Arb.Default.DoNotSizeInt16Arb.Default.DoNotSizeUInt64等.

对于浮点值,有Arb.Default.Float32,根据其文档,它生成相当频繁地包含任意浮点,NaN,NegativeInfinity,PositiveInfinity,Maxvalue,MinValue,Epsilon".. >

由于F#没有类型类(这是您可以在Haskell中表达的东西),因此没有统一的API可以任意"地使用任何数字.

此外,我不确定您的典型单元测试框架是否可以运行常规测试,但是至少对于xUnit.net,您可以使用

I thought it's time to try out FsCheck but it proves tougher than I thought. There's a lot of documentation on Arb, generators and so on, but there doesn't seem to be any guidance in how to apply that knowledge. Or I'm just not getting it.

What may make it harder to grasp is that the relation between tests, properties, generators, arbitraries, shrinking and, in my case, randomness (some tests automatically generate random data, others don't) is not clear to me. I don't have a Haskell background so that doesn't help much either.

Now for the question: how do I generate random integers?

My test scenario can be explained on the properties of multiplication, let's say distributivity:

static member  ``Multiplication is distributive`` (x: int64) y z =
    let res1 = x * (y + z)
    let res2 = x * y + x * z

    res1 = res2

// run it:
[<Test>]
static member FsCheckAsUnitTest() =
    Check.One({ Config.VerboseThrowOnFailure with MaxTest = 1000 }, ``Multiplication is distributive``)

When I run this with Check.Verbose or the NUnit integration, I get test sequences like:

0:
(-1L, -1L, -1L)
1:
(-1L, -1L, 0L)
2:
(-1L, -1L, -1L)
3:
(-1L, -1L, -1L)
4:
(-1L, 0L, -1L)
5:
(1L, 0L, 2L)
6:
(-2L, 0L, -1L)
7:
(-2L, -1L, -1L)
8:
(1L, 1L, -2L)
9:
(-2L, 2L, -2L)

After 1000 tests it hasn't gotten over 100L. Somehow I imagined this would "automatically" choose random numbers evenly distributed over the whole range of int64, at least that's how I interpreted the documentation.

Since it doesn't, I started experimenting and came up with silly solutions like the following to get higher numbers:

type Generators = 
    static member arbMyRecord =
        Arb.generate<int64>
        |> Gen.where ((<) 1000L)
        |> Gen.three
        |> Arb.fromGen

But this becomes incredibly slow and is clearly not the right approach. I'm sure there must be a simple solution that I'm missing. I tried with Gen.choose(Int64.MinValue, Int64.MaxValue), but this only supports ints, not longs (but even with just ints I couldn't get it working).

In the end I need a solution that works for all the primitive numeric data types, that includes their maxes and mins, their zeroes and ones, and some random selection from whatever is within.

解决方案

As explained in this other FsCheck question, the default configurations for most of the Check functions has EndSize = 100. You can increase that number, but you can also, as you suggest, use Gen.choose.

Even so, though, the int generator is intentionally well-behaved. It doesn't, for example, include Int32.MinValue and Int32.MaxValue, since this could lead to overflows.

FsCheck does, however, also come with generators that give you uniform distributions over their entire range: Arb.Default.DoNotSizeInt16, Arb.Default.DoNotSizeUInt64, and so on.

For floating point values, there's Arb.Default.Float32, which , according to its documentation, generates "arbitrary floats, NaN, NegativeInfinity, PositiveInfinity, Maxvalue, MinValue, Epsilon included fairly frequently".

There's no uniform API for 'just' any number, since F# doesn't have typeclasses (this is something you'd be able to express in Haskell).

Also, I'm not sure your typical unit testing framework will be able run generic tests, but at least with xUnit.net, you can use this trick to run generically typed tests.


Specifically, though, you can write the above test like this, using FsCheck.Xunit:

open FsCheck
open FsCheck.Xunit

[<Property>]
let ``Multiplication is distributive`` () =
    Arb.generate<DoNotSize<int64>>
    |> Gen.map (fun (DoNotSize x) -> x)
    |> Gen.three
    |> Arb.fromGen
    |> Prop.forAll <| fun (x, y, z) ->

        let res1 = x * (y + z)
        let res2 = x * y + x * z

        res1 = res2

This could hypothetically fail from overflowing, but after having run some 1,000,000 cases, I haven't seen it fail yet.

The generator, however, does indeed look like it's picking values from the full range of 64-bit integers:

> Arb.generate<DoNotSize<int64>> |> Gen.sample 1 10;;
val it : DoNotSize<int64> list =
  [DoNotSize -28197L; DoNotSize -123346460471168L; DoNotSize -28719L;
   DoNotSize -125588489564554L; DoNotSize -29241L;
   DoNotSize 7736726437182770284L; DoNotSize -2382327248148602956L;
   DoNotSize -554678787L; DoNotSize -1317194353L; DoNotSize -29668L]

Notice that even though I bind the size argument of Gen.sample to 1, it picks 'arbitrarily' large positive and negative values.

这篇关于如何使用FsCheck生成随机数作为基于属性的测试的输入的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆