jqwik strategies for combining arbitraries

446 views Asked by At

(Context: my background in property-based testing is mostly from scala's scalacheck library, the use of some of the types and annotations in jqwik feels a bit different and there are a couple paradigms I can't quite get the hang of yet.)

I'm not sure how best to combine existing Arbitrary definitions for primitive types to produce a re-usable (needed for more than a single @Property or test class) Arbitrary that is built atop other Aribtrary definitions I've defined.

Given this is probably much clearer with an illustration:

// assume this has a builder pattern or all-args constructor.
// consider this is some sort of core domain object that I need in various 
// test suites
public class MyComplexClass {
  private final String id; // positive-integer shaped
  private final String recordId; // uuid-shaped
  private final String creatorId; // positive-integer shaped
  private final String editorId; // positive-integer shaped
  private final String nonce; // uuid-shaped
  private final String payload; // random string
}

My instinct is to define Aribrary<String> that produces UUID-like strings and another that produces positive integer strings, something like:

public class MyArbitraries {
  public Arbitrary<String> arbUuidString() {
                return Combinators.combine(
                            Arbitraries.longs(), Arbitraries.longs(), Arbitraries.of(Set.of('8', '9', 'a', 'b')))
                    .as((l1, l2, y) -> {
                        StringBuilder b = new StringBuilder(new UUID(l1, l2).toString());
                        b.setCharAt(14, '4');
                        b.setCharAt(19, y);
                        return UUID.fromString(b.toString());
                    });
  }

  public Arbitrary<String> arbNumericIdString() {
    return Arbitraries.shorts().map(Math::abs).map(i -> "" + i);
  }
}

But then I'm not sure the best way to utilize these to produce an Arbitrary< MyComplexClass>. I'd want something like:

public class MyDomain extends DomainContextBase {
  @Provider
  public Arbitrary<MyComplexClass> arbMyComplexClass() {
    return Builders.withBuilder(MyComplexClass::newBuilder)
      // best way to reference these?!
      .use(arbNumericIdString()).in(MyComplexClass.Builder::setId)
      .use(arbUuidString()).in(MyComplexClass.Builder::setCreatorId)
      // etc.
    .build(MyComplexClass.Builder::build);
  }
}

My understanding here is:

  • I cannot use @ForAll to 'inject' or provide these Arbitraries as ForAll is only supported in @Property-annotated methods
  • I cannot use @Domain here for similar reasons
  • I can't really use ArbitrarySupplier or similar as there is no obvious 'type' here, it's mostly just a bunch of strings

Is the best option to just create static Arbitrary<String> functions and call them directly?

1

There are 1 answers

6
johanneslink On

One initial comment: @ForAll also works in methods annotated with @Provide and in domains. Here's a simple example:

class MyDomain extends DomainContextBase {

    @Provide
    public Arbitrary<String> strings(@ForAll("lengths") int length) {
        return Arbitraries.strings().alpha().ofLength(length);
    }

    @Provide
    public Arbitrary<Integer> lengths() {
        return Arbitraries.integers().between(3, 10);
    }

    // Will not be used by strings() method
    @Provide
    public Arbitrary<Integer> negatives() {
        return Arbitraries.integers().between(-100, -10);
    }

}

class MyProperties {
    @Property(tries = 101)
    @Domain(MyDomain.class)
    public void printOutAlphaStringsWithLength3to10(@ForAll String stringsFromDomain) {
        System.out.println(stringsFromDomain);
    }
}

Maybe the confusing thing is that the string reference in @ForAll("myString") is only evaluated locally (the class itself, superclasses and containing classes). This is by purpose, in order to prevent string-based reference magic; having to fall back to strings in the first place - since method refs cannot be used in Java annotations - is already bad enough.

As for your concrete question:

Is the best option to just create static Arbitrary functions and call them directly?

I consider that a "good enough" approach for sharing generators within a single domain or when you have several related domains that inherit from a common superclass.

When you want to share generators across unrelated domains, you'll have to either:

  • Use type-based resolution: Introduce value types for things like RecordId, UUIDString etc. Then you can use domains (or registered ArbitraryProviders to generate based on type.
  • Introduce annotations to mark different variants of the same type. You can then check the annotation in your provider method or arbitrary provider. Here's an example:
class MyNumbers extends DomainContextBase {
    @Provide
    Arbitrary<Integer> numbers() {
        return Arbitraries.integers().between(0, 255);
    }
}

@Domain(MyNumbers.class)
class MyDomain extends DomainContextBase {

    @Target({ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @interface Name {}

    @Target({ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @interface HexNumber {}

    @Provide
    public Arbitrary<String> names(TypeUsage targetType) {
        if (targetType.isAnnotated(Name.class)) {
            return Arbitraries.strings().alpha().ofLength(5);
        }
        return null;
    }

    @Provide
    public Arbitrary<String> numbers(TypeUsage targetType) {
        if (targetType.isAnnotated(HexNumber.class)) {
            return Arbitraries.defaultFor(Integer.class).map(Integer::toHexString);
        }
        return null;
    }
}

@Property(tries = 101)
@Domain(MyDomain.class)
public void generateNamesAndHexNumbers(
        @ForAll @MyDomain.Name String aName,
        @ForAll @MyDomain.HexNumber String aHexNumber
) {
    System.out.println(aName);
    System.out.println(aHexNumber);
}

This examples also shows how one domain (MyNumbers) can be used in another domain (MyDomain) through annotating the domain implementation class and either having a parameter being injected or use Arbitraries.defaultFor(TypeProvidedInOtherDomain.class).

But maybe there's a useful feature for sharing arbitraries missing in jqwik. The jqwik team's happy about any suggestion.