Использование FsCheck с NUnit: получение исключения при использовании произвольных типов (или: как использовать произвольные типы с атрибутами)

В моем предыдущем вопросе Курт указал мне на этот код FsCheck о настройке типа Arbitrary.

У меня есть следующее Arbitrary (отказ от ответственности: я понятия не имею, что я делаю..., по-прежнему нахожу FsCheck общеизвестно трудным для понимания, но я решительно настроен заставить его работать), что само по себе является упрощенной версией того, что я созданные ранее:

type MyArb() =
    inherit Arbitrary<DoNotSize<int64>>()
        override x.Generator = Arb.Default.DoNotSizeInt64().Generator

И я использую его по инструкции:

[<Property(Verbose = true, Arbitrary= [| typeof<MyArb> |])>]
static member  MultiplyIdentity (x: int64) = x * 1L = x

Это дает мне (несколько обнадеживающее) сообщение об ошибке, что я что-то упустил:

 System.Reflection.TargetInvocationException : Exception has been thrown by the target of an invocation.
  ----> System.Exception : No instances found on type Tests.Arithmetic.MyArb. Check that the type is public and has public static members with the right signature.
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at FsCheck.Runner.checkMethod(Config config, MethodInfo m, FSharpOption`1 target) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck\Runner.fs:line 318
   at FsCheck.NUnit.Addin.FsCheckTestMethod.runTestMethod(TestResult testResult) in C:\Users\Kurt\Projects\FsCheck\FsCheck\src\FsCheck.NUnit.Addin\FsCheckTestMethod.fs:line 100

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

Как я могу создать генератор случайных чисел и статически назначить его в качестве произвольного для моих тестов NUnit?


person Abel    schedule 06.12.2016    source источник


Ответы (3)


Тип, указанный в параметре Property.Arbitrary, должен иметь статические элементы (возможно, несколько) типа Arb. Как и в коде, который вы связали:

type TestArbitrary2 =
   static member NegativeDouble() =
       Arb.Default.Float()
       |> Arb.mapFilter (abs >> ((-) 0.0)) (fun t -> t <= 0.0)

Применив это к вашему коду, он должен выглядеть так:

 type MyArb() =
    static member m() = Arb.Default.DoNotSizeInt64()

Параметр Property.Arbitrary означает не "реализацию Arbitrary", а "набор реализаций классов типов".

Видите ли, исходная реализация QuickCheck на Haskell полагается на классы типов для предоставления значений разных типов. Чтобы конкретный тип можно было «быстро проверить», должен быть экземпляр класса «Произвольный», определенный для этого типа (например, здесь приведены экземпляры для всех основных типов).

Поскольку F# не поддерживает классы типов как таковые, FsCheck вынужден подделывать их, и здесь используется следующая схема: каждый экземпляр класса типов представлен статическим членом, который возвращает таблицу функций. Например, если мы хотим имитировать Eq typeclass< /a>, мы бы определили его примерно так:

type Eq<'a> = { eq: 'a -> 'a -> bool; neq: 'a -> 'a -> bool }

type EqInstances() =
   static member ForInt() : Eq<int> = 
      { eq = (=); neq = (<>) }

   static member ForMyCustomType() : Eq<MyCustomType> = 
      { eq = fun a b -> a.CompareTo(b) = 0
        neq = fun a b -> a.CompareTo(b) <> 0 }

Но поскольку вы не можете просто сканировать все статические элементы во всех загруженных сборках (это было бы непомерно дорого), существует небольшое неудобство, связанное с явным указанием типа (в качестве бонуса это позволяет контролировать видимость «экземпляров»).

person Fyodor Soikin    schedule 06.12.2016
comment
Так просто? Итак, никакого наследования, атрибутов и т. д.? Попытка... (редактировать: попробовал, работает мгновенно, спасибо!) - person Abel; 06.12.2016
comment
Спасибо за дополнительное объяснение, это имеет смысл. Однако static member m() никогда не вызывается (я пробовал с оператором printfn просто для уверенности). Это вполне может быть ошибка FsCheck (скорее всего, моя ошибка ;), но, как написано выше, она никогда не срабатывает. - person Abel; 06.12.2016
comment
Я понял. Мне нужно было развернуть тип DoNotSize, он не был распознан как запрошенный тип для рассматриваемой функции. То есть определение теперь Arb.Default.DoNotSizeInt64().Generator |> Gen.map DoNotSize.Unwrap |> Arb.fromGen. - person Abel; 06.12.2016

Этот вопрос ясно демонстрирует, ИМО, почему основанный на отражении API для FsCheck далеко не идеален. Я стараюсь полностью избегать этого API, поэтому вместо этого я бы написал свойство OP следующим образом:

open FsCheck
open FsCheck.Xunit

[<Property>]
let MultiplyIdentity () =
    Arb.Default.DoNotSizeInt64 () |> Prop.forAll <| fun (DoNotSize x) -> x * 1L = x

Как предполагают директивы open, здесь используется FsCheck.Xunit вместо FsCheck.NUnit, но, насколько мне известно, нет никакой разницы в том, как работает API.

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

person Mark Seemann    schedule 06.12.2016
comment
Мне это нравится. Только что попробовал. Я боялся, что тест будет unit, что многословие не сработает (но оно работает, и я вижу случайные int64). Хотя мне все еще любопытно, как решить исходную проблему (Федор сделал ее работоспособной, но метод не вызывается), этот подход выглядит лучшим с нескольких точек зрения, спасибо. - person Abel; 06.12.2016
comment
Кроме того, что интересно, только некоторые 5 из 100 тестов используют положительный int64 с этим кодом, остальные отрицательные. Это справедливо для нескольких запусков. Это по дизайну? (кстати, исходная проблема решена, я забыл развернуть, помогла ваша подсказка по безопасному типу) - person Abel; 06.12.2016
comment
@Abel Теперь, когда вы спрашиваете, этот генератор действительно выглядит перекошенным. Я не знаю, почему это так, но я создал вопрос об этом: github. com/fscheck/FsCheck/issues/332 - person Mark Seemann; 06.12.2016
comment
+1 Я также предпочитаю этот подход, который также ближе ко всем другим клонам на основе QuickCheck. Вы даже можете избавиться от атрибута [<Property>] и использовать функции модуля Check внутри теста. [<Fact>] — это все, что нужно xUnit.net для обнаружения теста на основе свойств. :) - person Nikos Baxevanis; 07.12.2016
comment
@NikosBaxevanis Разве это не требует, чтобы вы передали все свойство в Check.QuickThrowOnFailure? В прошлый раз, когда я пробовал этот подход, я нашел его несколько неудобным, но это было давно... Подумайте о том, чтобы добавить свой вариант в качестве ответа здесь;) - person Mark Seemann; 07.12.2016
comment
@MarkSeemann Да, для этого требуется передать все свойство в Check.QuickXyz. - person Nikos Baxevanis; 08.12.2016
comment
@MarkSeemann Передача в Check.QuickXyz делает вещи более явными. Неплохая идея, я думаю. - person Nikos Baxevanis; 09.12.2016
comment
Это кажется идеальным сочетанием NUnit + FsCheck. Без надстройки он будет слишком медленным для обработки, с надстройкой (через PropertyAttribute) и этим подходом у меня, кажется, есть лучшее из обоих миров (безопасность типов — сильный аргумент). И я все еще получаю журнал stdout. - person Abel; 09.12.2016

Если вы предпочитаете подход, описанный Марком Симаном, вы также можете рассмотреть возможность использования простого FsCheck и избавиться от FsCheck.Xunit. полностью:

module Tests

open FsCheck

let [<Xunit.Fact>] ``Multiply Identity (passing)`` () = 
    Arb.Default.DoNotSizeInt64 ()
    |> Prop.forAll
    <| fun (DoNotSize x) ->
        x * 1L = x
    |> Check.QuickThrowOnFailure

let [<Xunit.Fact>] ``Multiply Identity (failing)`` () = 
    Arb.Default.DoNotSizeInt64 ()
    |> Prop.forAll
    <| fun (DoNotSize x) ->
        x * 1L = -1L |@ sprintf "(%A should equal %A)" (x * 1L) x
    |> Check.QuickThrowOnFailure

Вывод xUnit.net testrunner:

------ Test started: Assembly: Library1.dll ------

Test 'Tests.Multiply Identity (failing)' failed: System.Exception:
    Falsifiable, after 1 test (2 shrinks) (StdGen (2100552947,296238694)):

Label of failing property: (0L should equal 0L)
Original:
DoNotSize -23143L
Shrunk:
DoNotSize 0L

    at <StartupCode$FsCheck>[email protected](String me..
    at <StartupCode$FsCheck>[email protected]..
    at FsCheck.Runner.check[a](Config config, a p)
    at FsCheck.Check.QuickThrowOnFailure[Testable](Testable property)
    C:\Users\Nikos\Desktop\Library1\Library1\Library1.fs(15,0): at Tests.Multi..

1 passed, 1 failed, 0 skipped, took 0.82 seconds (xUnit.net 2.1.0 build 3179).
person Nikos Baxevanis    schedule 08.12.2016
comment
На самом деле у меня был запущен NUnit + FsCheck, но я хотел, чтобы комбинация с Property упростила его настройку и давала более содержательный вывод в моем тестовом прогончике (OT: xUnit бесполезен для моего сценария, он не делать стандартные записи stdout и stderr, утверждает, что это мешает многопоточности (это так), в то время как я запускаю NUnit, с stdout, с многопоточностью, используя NCrunch). - person Abel; 09.12.2016
comment
И еще одно замечание: конвейерный подход имел странный побочный эффект в NUnit runner (только стандартный от NUnit, а не от NCrunch): он сообщает, что запускает тест за 0,12 с, но возвращается только через 10+ секунд (или дольше, это начинает заметно с 1000 или около того FsCheck работает с самой тривиальной функцией). У меня есть некоторое представление о том, в чем причина, и эта причина исправлена ​​​​в надстройке NUnit FsCheck (и я не нашел способа преобразовать это исправление в ванильный способ, который вы показали выше, с помощью xUnit). Следовательно, снова требуется желание использовать PropertyAttribute. - person Abel; 09.12.2016