Тестирование на основе свойств в F# с использованием условных параметров

В настоящее время я пишу тест на основе свойств для проверки функции расчета скорости в f # с 4 параметрами с плавающей запятой, и все параметры имеют определенные условия для их правильности (например, a > 0,0 && a ‹ 1,0 и b > a) . У меня есть функция, проверяющая выполнение этих условий и возвращающая логическое значение. У меня вопрос: в моем тестовом коде, использующем [Property>] в FsCheck.Xunit, как мне ограничить генератор для проверки кодов, используя только значения, соответствующие моим конкретным условиям для параметров?


person wolfffff    schedule 21.02.2019    source источник


Ответы (2)


Если вы используете FsCheck, вы можете использовать функцию Gen.filter и функцию Gen.map.

Допустим, у вас есть функция funToBeTested, которую вы тестируете, для которой требуется, чтобы a ‹ b:

let funToBeTested a b = if a < b then a + b else failwith "a should be less than b" 

И вы проверяете свойство, которое funToBeTested пропорционально входным данным:

let propertyTested a b = funToBeTested a b / 2. = funToBeTested (a / 2.) (b / 2.)

У вас также есть предикат, который проверяет требования условия для a и b:

let predicate a b = a > 0.0 && a < 1.0 && b > a

Мы начинаем с генерации float чисел, используя Gen.choose и Gen.map, таким образом уже получаются значения только от 0.0 до 1.0:

let genFloatFrom0To1 = Gen.choose (0, 10000) |> Gen.map (fun i -> float i / 10000.0 )

Затем мы генерируем two чисел с плавающей запятой от 0 до 1 и filter их с помощью функции predicate выше.

let genAB            = Gen.two genFloatFrom0To1 |> Gen.filter (fun (a,b) -> predicate a b )

Теперь нам нужно создать новый тип TestData для использования этих значений:

type TestData = TestData of float * float

и мы сопоставляем полученное значение с TestData

let genTest = genAB |> Gen.map TestData

Далее нам нужно зарегистрировать genTest в качестве генератора для TestData, для этого мы создадим новый класс со статическим членом типа Arbitrary<TestData>:

type MyGenerators =
    static member TestData : Arbitrary<TestData> = genTest |> Arb.fromGen

Arb.register<MyGenerators>() |> ignore

наконец, мы проверяем свойство, используя TestData в качестве входных данных:

Check.Quick (fun (TestData(a, b)) -> propertyTested a b )

ОБНОВИТЬ:

Простой способ составить разные генераторы — использовать gen Computation Expression:

type TestData = {
    a : float
    b : float 
    c : float 
    n : int
}

let genTest = gen {
    let! a = genFloatFrom0To1
    let! b = genFloatFrom0To1
    let! c = genFloatFrom0To1
    let! n = Gen.choose(0, 30)
    return {
        a = a
        b = b
        c = c
        n = n
    }
}

type MyGenerator =
    static member TestData : Arbitrary<TestData> = genTest |> Arb.fromGen

Arb.register<MyGenerator>() |> ignore


let ``Test rate Calc`` a b c n =                               
    let r = rCalc a b c
    (float) r >= 0.0 && (float) r <= 1.0    


Check.Quick (fun (testData:TestData) -> 
    ``Test rate Calc`` 
        testData.a 
        testData.b 
        testData.c 
        testData.n)         
person AMieres    schedule 21.02.2019
comment
Привет, я следовал этой логике в своем коде, но вместо Check.Quick мне пришлось использовать [‹Property (Arbitrary = [|typeof‹MyGenerators›|])›], потому что я продолжал получать тесты, которые не могут быть найдены при запуске с Check .Быстрый. Однако, когда я запускаю тест, он продолжает давать сбой, потому что числа, сгенерированные в тесте, все еще выходят за пределы (я часто получал -бесконечность в одном из a, b). Я подозреваю, что это произошло из-за того, что пункт [‹Property (Arbitrary...)›] каким-то образом не работал должным образом. Вы знаете, что может происходить? Спасибо еще раз - person wolfffff; 22.02.2019
comment
Ваша функция свойства Test rate Calc должна получить TestData(a, b, c) в качестве параметра, в настоящее время она получает a b c. - person AMieres; 22.02.2019
comment
Оно работает! Благодарю вас! Если вы не возражаете, что я задам еще один вопрос, если я хочу добавить к типу TestData другую переменную int из Gen.choose(1,30), чтобы тестовые данные были float* float * float*int, как мне изменить genTest или genAB для отображения нового типа TestData? - person wolfffff; 22.02.2019
comment
Я добавил способ сделать это в конце ответа. Я также изменил TestData с DU на запись, потому что это становится беспорядочным, когда в DU слишком много полей. - person AMieres; 23.02.2019

Ответ @AMieres - отличное объяснение всего, что вам нужно, чтобы решить эту проблему!

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

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

Если вы можете написать это так, чтобы всегда генерировать допустимые значения, то это немного лучше. Моя версия для этого конкретного случая состояла бы в том, чтобы использовать map для замены чисел, чтобы меньшее всегда было первым:

let genFloatFrom0To1 = Gen.choose (0, 10000) |> Gen.map (fun i -> float i / 10000.0 )
let genAB = Gen.two genFloatFrom0To1 |> Gen.map (fun (a, b) -> min a b, max a b)
person Tomas Petricek    schedule 21.02.2019
comment
Это элегантное решение! - person AMieres; 22.02.2019