Как использовать 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), но это поддерживает только целые, а не длинные (но даже с одними целыми я не мог заставить его работать).

В конце концов мне нужно решение, которое работает для всех примитивных числовых типов данных, которое включает их максимумы и минимумы, их нули и единицы, а также некоторый случайный выбор из того, что находится внутри.

1 ответ

Решение

Как объяснено в этом другом вопросе FsCheck, конфигурации по умолчанию для большинства Check функции имеет EndSize = 100, Вы можете увеличить это число, но вы также можете, как вы предлагаете, использовать Gen.choose,

Несмотря на это, хотя, int Генератор намеренно хорошо себя ведет. Это, например, не включает Int32.MinValue а также Int32.MaxValue, так как это может привести к переполнению.

FsCheck, однако, также поставляется с генераторами, которые дают вам равномерное распределение по всему диапазону: Arb.Default.DoNotSizeInt16, Arb.Default.DoNotSizeUInt64, и так далее.

Для значений с плавающей запятой есть Arb.Default.Float32, который, согласно его документации, генерирует "произвольные числа с плавающей точкой, NaN, NegativeInfinity, PositiveInfinity, Maxvalue, MinValue, Epsilon включены довольно часто".

Не существует единого API для "просто" любого числа, поскольку F# не имеет классов типов (это то, что вы сможете выразить в Haskell).

Кроме того, я не уверен, что ваша типовая среда модульного тестирования сможет запускать общие тесты, но, по крайней мере, с xUnit.net, вы можете использовать этот трюк для запуска тестов общего типа.


В частности, вы можете написать приведенный выше тест следующим образом, используя 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

Это может гипотетически потерпеть неудачу из-за переполнения, но после запуска около 1 000 000 случаев, я еще не видел, как это не удалось.

Генератор, однако, действительно выглядит так, как будто он выбирает значения из всего диапазона 64-битных целых чисел:

> 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]

Обратите внимание, что хотя я связываю size аргумент Gen.sample в 1он выбирает "произвольно" большие положительные и отрицательные значения.

Другие вопросы по тегам