Skip to content
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

Generate values outside of property parameters #211

Open
sir4ur0n opened this issue Dec 21, 2018 · 5 comments
Open

Generate values outside of property parameters #211

sir4ur0n opened this issue Dec 21, 2018 · 5 comments
Labels

Comments

@sir4ur0n
Copy link
Contributor

Hi,

I'm wondering if there's an easy way to invoke generators to build random values outside of property parameters.
I see 2 use cases:

  1. For all my models, I have created the associated generators using either Generator or ComponentizedGenerator and so far, they seem to work great on properties.
    However sometimes, the generated values need a specific value in one or several of its fields, for a single property test. This seems overkill to create a new generator specifically for this issue.
    Also, we never ever use mutability (no setters) so I can't use setters. We use immutable modifications (which create new instances) but this would force us to create a second variable with a new name.
// foo needs some specific values for its bar and baz fields
@Property
public void property(Foo foo) {
  // Option 1: mutation, which we can't and don't want to do
  foo.setBar(new Bar()).setBaz(new Baz());
  // Option 2: immutable setters (called Withers in Lombok), but we are forced to create a new variable with a new name, which pollutes the namespace in scope, and forces the writer to think of many unnecessary names
  Foo foo2 = foo.withBar(new Bar()).withBaz(new Baz());
}
  1. We often want to generate random values/POJOs for unit tests, not just for property tests. It is frustrating to have built so many generators that would also perfectly fit the unit test case, but there's no simple way to consume it. It would be nice to be able to easily invoke the generators.
    I found an ugly way to do it by looking at your code and came with this:
SourceOfRandomness sourceOfRandomness = new SourceOfRandomness(new Random());
    FooGen fooGen = new FooGen();
    fooGen.provide(new GeneratorRepository(sourceOfRandomness)
        .register(new ServiceLoaderGeneratorSource()));
    Foo generated = fooGen.generate(sourceOfRandomness, new SimpleGenerationStatus(new GeometricDistribution(),
        sourceOfRandomness, 0));

Besides the fact that I could extract it in a generic method, there's the issue of edge cases: e.g. for a field of type String, the first random value is always the edge case "" (empty string), so this is not good enough.
Another alternative is to write a whole new set of agnostic generators (e.g. based on random-beans) but it means we have to write all generators twice, which is a waste of time/energy.

What's your opinion/recommendation here please?
Thanks

@jlink
Copy link

jlink commented Dec 21, 2018

It's an interesting problem which I have been considering in the context of jqwik.net.

What we'd like to have is a combination of random generators and the test data builder pattern described in the GOOS book. Here's an old article by one of the GOOS authors: http://natpryce.com/articles/000714.html

It looks rather simple at first glance since all we have todo is to replace the predefined default values in builder objects by calls to random generators. And indeed, it would be easy if we started with the builders and use random generators inside. However, starting with the already constructed random generator we'd have to deconstruct it to get into the nested parts which is not that easy. All I could think of so far - without deconstructing the generator - is something like that:

public class Builder<T> {
	public static <T> Builder<T> from(Gen<T> generator) {
		return new Builder<T>(generator);
	}

	private final Arbitrary<T> generator;
	private final List<Function<T, T>> transformers = new ArrayList<>();

	public Builder(Gen<T> generator) {this.generator = generator;}

	public T build(SourceOfRandomness random) {
		T value = generator.generate(random, new SimpleGenerationStatus()).value();
		return transform(value, transformers);
	}

	private T transform(T value, List<Function<T, T>> transformers) {
		if (transformers.isEmpty()) {
			return value;
		}
		T transformedValue = transformers.remove(0).apply(value);
		return transform(transformedValue, transformers);
	}

	public Builder<T> with(Function<T, T> transformer) {
		transformers.add(transformer);
		return this;
	}
}

But this requires indeed some way to transform an already existing value. I'm hoping you folks'll come up with something better.

@pholser
Copy link
Owner

pholser commented Jan 2, 2019

@sir4ur0n Thanks for your interest! Perhaps we can come up with a way to present code other than property methods with the ability to generate random values.

@pholser
Copy link
Owner

pholser commented Jan 3, 2019

@sir4ur0n @jlink I'm struggling with how to tease the ability to generate a random value not under the influence of a size parameter or config annotations from a Generator; then, with how to offer arbitrary (pun not intended) code to get a handle on such a generation ability.

For point 1) above: this may complicate individual generators a bit, but I wonder if adding config annotations to a generator to influence where specific composite values should be added would help?

    class Foo {
        Foo withBar(Bar b) { /* ... */ }
        Foo withBaz(Baz z) { /* ... */ }
        // ...
    }

    class FooGen extends Generator<Foo> {
        // ...

        // where `@WithFixed` is a container annotation of `@With`,
        // both of which you write
        public void configure(@WithFixed fixed) {
            // make a Foo, possibly with fixed attributes as dictated
        }
    }

   // ...

   @Property public void property(
        @WithFixed(
            @With(bar = "some-ref-that-can-be-resolved-to-the-bar-you-want")
            @With(baz = "some-ref-that-can-be-resolved-to-the-bar-you-want")
        ) Foo f) {
            // ...
        }

It moves the suck of building using the fixed values to the generators, with a cost of adding suck at the property declaration. Once junit-quickcheck supports meta-annotations, maybe the suck at the property site would be less. All of this seems very hoop-jumpy tho.

@kPOWz
Copy link

kPOWz commented Aug 7, 2020

I love this library & would also like to use the power of the generators in different contexts - particularly arbitrary objects/POJOs in tests that are not using property based testing.

Taking on the current Counter example w/ Fields as a basis ...

@RunWith(JUnitQuickcheck.class)
public class CounterPropertiesTest {
  @Property public void incrementing(@From(Fields.class) Counter c) {
    int count = c.count();
    assertEquals(count + 1, c.increment().count());
  }

  @Property public void decrementing(@From(Fields.class) Counter c) {
    int count = c.count();
    assertEquals(count - 1, c.decrement().count());
  }
}

I'd love to be able to write something like...

@ExtendWith(JUnitQuickcheckGenerators.class)  // or some other form of mixin
public class CounterNotPropertiesTest {
  public void incrementing() {
    Fields<Counter> fields = new Fields(Counter.class);
    Counter c = fields.generate();
    int count = c.count();

    assertEquals(count + 1, c.increment().count());
  }

  public void decrementing() {
    Fields<Counter> fields = new Fields(Counter.class);
    Counter c = fields.generate();
    int count = c.count();

    assertEquals(count - 1, c.decrement().count());
  }
}

Where JUnitQuickcheckGenerators would trigger the bootstrapping of the generator context

@pholser
Copy link
Owner

pholser commented Aug 10, 2020

@kPOWz I wonder if https://glytching.github.io/junit-extensions/randomBeans might better suit your needs in this case?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants