javatestingjunit5jqwik

How to write a jqwik generator method with nested generators


Using jqwik.net, trying to generate a Rule class with a a nested RuleConfig class inside it. The RuleConfig class has a nested ruleProps which is a Map

The statusReturnedFromApplyingRule method always returns an initialized Rule instead of using the @provide method values ?? Returned Rule: rule:Rule{ruleId='null', inputMetricSelector=null, ruleConfig='RuleConfig{ruleType='null', ruleProps={}}'}, elements:[{}]

Here is my code:

public class RangeMatchRuleTest {

    @Property
    @Report(Reporting.GENERATED)
    boolean statusReturnedFromApplyingRule(@ForAll("generateRule") Rule rule,
                                           @ForAll("generateInputMapElements") Iterable<Map<String, Object>> elements) {
        RangeMatchRule rangeMatchRule = new RangeMatchRule();
        final RuleIF.Status status = rangeMatchRule.applyRule(rule, elements);
        return RuleIF.getEnums().contains(status.toString());
    }

    @Provide
    Arbitrary<Rule> generateRule() {
        Rule rule = new Rule();
        RuleConfig ruleConfig = new RuleConfig();
        Map<String, Object> ruleProps = new HashMap<>();

        Arbitrary<Double> lowThresholdArb = Arbitraries.doubles()
                .between(0.0, 29.0);
        lowThresholdArb.allValues().ifPresent(doubleStream -> ruleProps.put(Utils.LOW_THRESHOLD, doubleStream.findFirst().get()));
        //lowThresholdArb.map(lowThreshold -> ruleProps.put(Utils.LOW_THRESHOLD, lowThreshold) );
        Arbitrary<Double> highThresholdArb = Arbitraries.doubles()
                .between(30.0, 50.0);
        highThresholdArb.map(highThreshold -> ruleProps.put(Utils.HIGH_THRESHOLD, highThreshold));
        ruleConfig.setRuleProps(ruleProps);
        rule.setRuleConfig(ruleConfig);
        return Arbitraries.create(() -> rule);
    }

    @Provide
    Arbitrary<Iterable<Map<String, Object>>> generateInputMapElements() {
        Arbitrary<Double> metricValueArb = Arbitraries.doubles()
                .between(0, 50.0);
        Map<String, Object> inputMap = new HashMap<>();
        metricValueArb.map(metricValue -> inputMap.put(Utils.METRIC_VALUE, metricValue));
        List<Map<String, Object>> inputMapLst = new ArrayList<>();
        inputMapLst.add(inputMap);
        return Arbitraries.create(() -> inputMapLst);
    }
}

TIA


Solution

  • You are building the generateRule method on the wrong assumption that an arbitrary's map method performed any real action when called. This is not the case. The fact that map returns another arbitrary instance gives a strong hint.

    The underlying idea you have to grasp is that a provider method - the method annotated with @Provide - is nothing but a "description" of the generation process; it will only be called once. The actual object generation happens afterwards and is controlled by the framework.

    Here's a reworked generateRule method that should do what you intended:

    @Provide
    Arbitrary<Rule> generateRule() {
        Arbitrary<Double> lowThresholdArb = Arbitraries.doubles()
                                                       .between(0.0, 29.0);
        Arbitrary<Double> highThresholdArb = Arbitraries.doubles()
                                                        .between(30.0, 50.0);
    
        Arbitrary<RuleConfig> configArb =
            Combinators.combine(lowThresholdArb, highThresholdArb)
                       .as((low, high) -> {
                           Map<String, Object> ruleProps = new HashMap<>();
                           ruleProps.put(Utils.LOW_THRESHOLD, low);
                           ruleProps.put(Utils.HIGH_THRESHOLD, high);
                           RuleConfig ruleConfig = new RuleConfig();
                           ruleConfig.setRuleProps(ruleProps);
                           return ruleConfig;
                       });
    
        return configArb.map(config -> {
            Rule rule = new Rule();
            rule.setRuleConfig(config);
            return rule;
        });
    }
    

    What you can hopefully see is that creating a generator is like dataflow programming: Starting from some base arbitraries - lowThresholdArb and highThresholdArb - you combine, map and filter those. In the end a single instance of Arbitrary must be returned.

    BTW: If you want this generator to be applied each time when you need a Rule, you could write the following class:

    public class RuleArbitraryProvider implements ArbitraryProvider {
    
        @Override
        public boolean canProvideFor(TypeUsage targetType) {
            return targetType.isOfType(Rule.class);
        }
    
        @Override
        public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
            return Collections.singleton(generateRule());
        }
    
        private Arbitrary<Rule> generateRule() {
            // Put here the code from above
            ...
        }
    }
    

    and register it as a default provider.