Как использовать 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
он выбирает "произвольно" большие положительные и отрицательные значения.