jqwik using @ForAll with collection in @Provide-annotated method

359 views Asked by At

Having a hard time figuring out how to utilize @ForAll in jqwik on a @Provide function accepting a collection.

Consider:

// domain model
public class Name {
  public final String first;
  public final String last;
  public Name(String f, String l) { 
    this.first = f;
    this.last = l;
  }
}

// jqwik domain context
public class NameDomain extends DomainContextBase {
  @Provide
  public Arbitrary<Name> arbName() {
    return Combinators.combine(
      Arbitraries.strings().alpha(), 
      Arbitraries.strings().alpha()
    ).as(Name::new);
  }
}

// properties test
public class NameProperties {
  // obviously a made-up use case, just demonstrating the issue
  @Provide
  @Domain(NameDomain.class)
  public Arbitrary<Set<String>> namesToParse(
    @ForAll @Size(min = 1, max = 4) Set<Name> names) {
    // ... code here
  }

  @Property
  public void namesAreParsed(@ForAll("namesToParse") Set<String> names) {
    // ... code here
  }
}

When running this, I end up with:

net.jqwik.api.CannotFindArbitraryException: Cannot find an Arbitrary for Parameter of type [@net.jqwik.api.ForAll(value="", supplier=net.jqwik.api.ArbitrarySupplier$NONE.class) @net.jqwik.api.constraints.Size(value=0, max=4, min=1) Set] in method [public net.jqwik.api.Arbitrary mypackage.NameProperties.namesToParse(java.util.Set)]

Very similar issues attempting to use @UniqueElements List<Name> instead. What am I missing here?

1

There are 1 answers

3
johanneslink On BEST ANSWER

What you are missing is that the @Domain annotation can only be applied to property methods or their container class. What should therefore work is:

@Property
@Domain(NameDomain.class)
public void namesAreParsed(@ForAll("namesToParse") Set<String> names) {
    // ... code here
}

or

@Domain(NameDomain.class)
class NameProperties { ... }

That said, you should be aware that using @ForAll params in a providing method will always use flat mapping over the injected parameters. Don't use that if you actually want to just map over or combine the injected parameters. In that case your providing method would look something like:

@Provide
public Arbitrary<Set<String>> namesToParse() {
    SetArbitrary<Name> names = Arbitraries.defaultFor(Name.class)
                                          .set().ofMinSize(1).ofMaxSize(4);
    // Code here just an example of what could be done:
    return names.mapEach((Set<Name> ignore, Name n) -> n.first + " " + n.last);
}