pholser/junit-quickcheck

Best practice for delegating configurations?

941design opened this issue · 3 comments

Hi,

How to properly delegate a generator configuration?

Assume MyObject has two attributes of the same type whose generators need to be configured. Let's say foo and bar are both integers, and I want to configure them with different ranges. I created a configuration annotation with fields fooMin, fooMax, barMin, and barMax, and a generator MyObjectGen that can be configured with MyObjectConf. In configure I want to delegate to sub-generators. What is the proper way to do this?

My solution works but does not feel right, because I also hat to create an implementation of @InRange in order to configure the inner generators. I know I could configure the inner generators with annotations, but that does not seem to be practical either (when I have a lot of test methods each requiring a unique configuration).

On the other hand, I like the usage of my implementation, because how the parameter is configured matches that of the built-in generated parameters.

What am I missing?
Thanks a lot!

@RunWith(JUnitQuickcheck.class)
public class MyObjectPropertiesTest {

    @Target({PARAMETER, FIELD, ANNOTATION_TYPE, TYPE_USE})
    @Retention(RUNTIME)
    @From(MyObjectGen.class)
    @GeneratorConfiguration
    @interface MyObjectConf {

        int fooMin() default 0;

        int fooMax() default 10;

        int barMin() default 0;

        int barMax() default 10;

    }

    public static class MyObject {

        private final int foo;
        private final int bar;

        public MyObject(int foo, int bar) {
            this.foo = foo;
            this.bar = bar;
        }

    }

    public static class MyObjectGen extends Generator<MyObject> {

        private final IntegerGenerator fooGen = new IntegerGenerator();
        private final IntegerGenerator barGen = new IntegerGenerator();

        public MyObjectGen() {
            super(MyObject.class);
        }

        public void configure(MyObjectConf config) {
            fooGen.configure(new InRangeBase() {
                @Override
                public String min() {
                    return String.valueOf(config.fooMin());
                }

                @Override
                public String max() {
                    return String.valueOf(config.fooMax());
                }
            });
            barGen.configure(new InRangeBase() {
                @Override
                public String min() {
                    return String.valueOf(config.barMin());
                }

                @Override
                public String max() {
                    return String.valueOf(config.barMax());
                }
            });
        }

        @Override
        public MyObject generate(SourceOfRandomness random, GenerationStatus status) {
            return new MyObject(fooGen.generate(random, status),
                                barGen.generate(random, status));
        }

    }

    @Property
    public void lessThanZero(@MyObjectConf(fooMin = -10, fooMax = -1) MyObject obj) {
        assertThat(obj.foo, lessThan(0));
    }

}

@941design Thanks for this -- sorry I'm just getting around to reading it. Will advise.

@941design I think using the random passed to generate() directly might be your best bet:

  public static class MyObjectGen extends Generator<MyObject> {
    private int fooMin, fooMax, barMin, barMax;

    public MyObjectGen() {
      super(MyObject.class);
    }

    public void configure(MyObjectConf config) {
      this.fooMin = config.fooMin();
      this.fooMax = config.fooMax();
      this.barMin = config.barMin();
      this.barMax = config.barMax();
    }

    @Override
    public MyObject generate(
      SourceOfRandomness random,
      GenerationStatus status) {

      return new MyObject(
        random.nextInt(fooMin, fooMax),
        random.nextInt(barMin, barMax));
    }
  }

@941design Let me know how this works for you -- if satisfactory, I'll close the issue. Thanks!