-
Notifications
You must be signed in to change notification settings - Fork 158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Prop API and testable values #397
Comments
IMHO stuff like this is a show stoper (first example from docs) let revRevIsOrig (xs:list<int>) = List.rev(List.rev xs) = xs
Check.Quick revRevIsOrig having it like that let revRevIsOrig (xs:list<int>) = if List.rev(List.rev xs) = xs then () else failwith "blahblah"
Check.Quick revRevIsOrig would be harder to sell it to new users. |
Could be:
Or with an assertion framework like Unquote you get brevity and the property back, and better error messages:
Overall I've come to dislike returning bools since it doesn't scale beyond this very simple example. When a test that returns bool fails, it gives me zero helpful information. Especially since I tend to combine using EDIT: And btw, it's literally a one liner to add "support" for bool return types back:
Note I've exactly simulated the assertion failure message as well ;) |
Yep, or with unqote: let revRevIsOrig (xs: int list) = test <@ List.rev(List.rev xs) = xs @> On a side note. I've had nested properties before (translated a library from haskell that used quickcheck), which I don't know how I would have gotten around. I don't remember the exact usecase, but it was something to the tune of this (note this is a trivial example which somewhat illustrates the need): type Vector3D = { x: int; y: int; z: int }
type Operation = Add | Multiply
let test (op: Operation) =
match op with
| Add -> Prop.forAll(fun (v1: Vector3D) (v2: Vector3D) -> check stuffs)
| Multiply -> Prop.forAll(fun (v: Vector3D) (n: int) -> check other stuffs) Now, in this trivial example, it would be easy to split them apart, but I remember not being able to figure a clean way to do that with the test I had back then. Sorry it's vague, but it's been a while :-/. That being said, if you used a state monad for the tests, you could make sure that nested |
Usually it's used as a somewhat convenient way to parametrize an Arbitrary instance according to the result of an outer Arbitrary or some calculation in the test (or even some earlier test result). Note it also allows recursive properties. So yes agree there are some usages here.
That is already the case. |
Some of this sounds fine to me; some of it concerns me a bit. I'm not going to explicitly call out places where I agree, because I agree with much of it, and there's no reason to reiterate it all. On the other hand, I have some concerns: On simpler return typesUsing only exceptions to signal failures doesn't sound functional to me. I agree that currently, all .NET unit testing assertion libraries tend to rely on exceptions, but that's mostly for lack of better options - historically. Now that the F# core library has an official So, according to that, I think that On removing
|
I know where you're coming from with the functional comment. It's exactly what I believed. But now I believe that actually, exceptions are the best option, and not even in a "least worst" kind of way. Exceptions are a good way to implement assertions because they allow you to break early, they add context (stacktrace and message), and compose easily (no workflow or monad needed). Then there is overwhelming ecosystem support in the CLR obviously, but also in test runners (e.g. click on stacktrace to go to code). See also: https://eiriktsarpalis.wordpress.com/2017/02/19/youre-better-off-using-exceptions/ |
Yes... there would have to be an let ``Validate.reservation returns right result on invalid date``() =
Prop.forAll (Arb.from<ReservationRendition> |> Arb.where (fun r -> not(fst(DateTimeOffset.TryParse rendition.Date)))) (fun rendition ->
let actual = Validate.reservation rendition
let expected : Result<Reservation, Error> =
Failure(ValidationError("Invalid date."))
test <@ expected = actual @>) Which is somehow somewhat less pleasing to the eye, but overall seems to move things more where they belong. This would have the advantage that discarding values gets pushed into One risk here is that people write things like the below in their actual test if <precondition> then
<actual test> which would carry the risk that the test maybe becomes slow for unclear reasons because too many discarded values, and there is no way for FsCheck to detect this. |
Yes, this is a major downside. The change to go from a simple test (maybe a first version without the nested property) to a more "advanced" one would be very discontinuous - i.e. a relatively simple addition with nested properties, would involve a more significant refactoring, with more plumbing as you describe. |
Yes, I know of that blog post, and I also left a comment there stating my position. It hasn't changed since then. .NET exceptions are good for some scenarios, but I think it'd be a shame to write off the ability to use Either. |
I personally don't think that using So, I hereby tone down this proposal to:
I hope this is uncontroversial 😄 For |
How about just having it be [Edit] |
Hmm, yeah. Is it worth inflicting the conservative choice on everyone to save some confusion...too close to call? |
I am going to close this, as it's been decided that this will stay more or less as is and I've applied the smaller changes that were going through. There are some question marks around Any further thoughts on this feel free to keep commenting or open a separate issue. |
I propose to significantly simplify the
Prop
API and what FsCheck considers to be testable values.For one, FsCheck can currently check functions that are "testable", which is to say they return
unit
orvoid
,bool
,Property
,Lazy<T>
,Func<TArb,T>
where T is testable. On the fscheck3 branch we addedTask<T>
. (There are also a bunch of other types that are really only there to make the implementation work.)The API on
Prop
is basically composed of:Arbitrary
and a function to test.trivial
,collect
which show how values are distributed on successful testwithin
.&.
(and) and.|.
(or)label
function to show a string on a failing test.In short, my suggestion would be to simplify this API away to almost nothing - basically to just
Prop.forAll
with different functions/overloads for a number of arities and return types.Failure is signaled only via throwing exceptions and so functions returningbool
etc are not allowed. EvenProperty
would be disallowed.Some reasoning and ramifications:
Testable
generic type.Prop.forAll: Arbitrary<T> -> (T -> unit) -> Property
Prop.forAllAsync: Arbitrary<T> -> (T -> Task) -> Property
Prop.forAllFsAsync: Arbitrary<T> -> (T -> Async<unit>) -> Property
unit
is natural type to have as a return type. Also the advantage of signalling failure via an exception is manifold:a = b
where the result isfalse
, with exceptions you can add an error message like "Expected a, got b". So this essentially makeslabel
superfluous..&.
and.|.
operators superfluous.==>
because it is confusing - because of eager evaluation the RHS gets evaluated regardless of whether the LHS succeeds or not. It is also the main reason whyLazy
is a supported testable type - so you can use... ==> lazy (...)
. Instead, filtering of the values to be tested should happen on theArbitrary
side which perhaps looks slightly less nice - and I'm not even sure about that - but works much better in practice. Moving discarded values - i.e. that don't match the filter - to earlier in the chain like inGen.where
andGen.filter
would also allow FsCheck to warn in all cases when a particular filter is too strict. Right now this warning only works if you use==>
, not with the methods onGen
.within
andthrows
are really not FsCheck's core business - let's point users to a better alternative in other libraries like Unquote, xUnit.NET etc.trivial
,classify
andcollect
should be replaced by making the full list of generated objects available as the output of an FsCheck test run, so that any analysis of the generated values can happen afterwards, and perhaps using more powerful tools than just text output (e.g. draw a histogram). This also allows other scenarios like storing the values somewhere for later reproduction/analysis etc.Property
as an allowed return type essentially removes the ability to write nestedProp.forAll
calls. I think this imposes a stricter separation of value generation and testing, which doesn't take away actual expressive power, but imposes some syntactic limitations that could be seen as a nuisance but are long-term and overall beneficial. It also makes setting up a test very uniform: aProp.forAll
call taking one or more Arbitrary instances, a test function, and optionally some post processing and analysis of the generated values. However this is the most controversial aspect of the suggestion. I have a feeling it actually does take away expressive power, will have to try.discard
. Even though it is not used often, it represents a legitimate use case.The text was updated successfully, but these errors were encountered: