jqwik-team / jqwik

Property-Based Testing on the JUnit Platform
http://jqwik.net
Eclipse Public License 2.0
578 stars 64 forks source link

Negate arbitrary #417

Open lmartelli opened 2 years ago

lmartelli commented 2 years ago

Testing Problem

When testing some validation rules, it's easy to come up with a custom arbitrary for the OK case, but much more complicated for the failing cases. Say you want to test isValidUsername(String) which must be true for any String of size 3-10 with only lowercase ASCII letters. strings().ofLength(3..10).withCharRange('a'..'z') will do to test the case where the result should be true. But in order to to test the case where it should return false, you will have to manually split into subcases (length < 3, length > 10, and non lowercase letters)

Suggested Solution

It would be super nice to be able to negate an arbitrary, maybe like this : @ForAll("username", negated = true).

jlink commented 2 years ago

Would you be able to specify what negated behaviour is in the general case?

Even if I look at your supposedly obvious (?) example, some questions arise about what is part of negated behaviour:

What I can imagine is to have a base generator and then apply a filter and its negation, e.g.:

var base = strings().ascii();
var valid = base.filter(pw -> pw.chars().allMatch( c -> ...) && pw.length() >= 3 && ...);
var inValid = valid.negated();

I may be missing something, though.

lmartelli commented 2 years ago

Maybe it would be better and more general to think in terms of difference :

valid = strings().ascii().ofMinLength(3).ofMaxLength(10)
invalid = strings().difference(valid)

So that if for all x in strings(), x is either in valid or in invalid (but not both).

jlink commented 2 years ago

Thinking of implementation: I can imagine this to work for arbitraries of the same kind, e.g. stringArbitrary.except(anotherStringArbitrary) or integerArbitrary.except(otherIntegerArbitrary)

But as soon as there's a generic thing involved, like filtering, mapping, or combining I don't see a way to implement it.

I wonder if this very restricted application is worth the effort since even the string difference implementation looks like quite involved programming to cover all the potential edge cases and pitfalls.

lmartelli commented 2 years ago

I must confess, that although I have the intuition that it should be computable, I have no clue how to do it 😄

jlink commented 2 years ago

The big problem from an implementation side is that there is no generic way to determine if an object of type T could potentially be generated by an arbitrary of type Arbitrary<T>.

So, the one thing from this issue that looks like being doable with reasonable effort is the suggestion from above:

var base = strings().ascii();
var valid = base.filter(pw -> pw.chars().allMatch( c -> ...) && pw.length() >= 3 && ...);
var inValid = valid.negated();