| File: | blib/lib/App/Test/Generator/TestStrategy.pm |
| Coverage: | 90.8% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package App::Test::Generator::TestStrategy; | |||||
| 2 | ||||||
| 3 | 9 9 9 | 65214 8 104 | use strict; | |||
| 4 | 9 9 9 | 14 6 139 | use warnings; | |||
| 5 | 9 9 9 | 201 1659 3053 | use Readonly; | |||
| 6 | ||||||
| 7 | # -------------------------------------------------- | |||||
| 8 | # Accessor type strings from the schema | |||||
| 9 | # -------------------------------------------------- | |||||
| 10 | Readonly my $ACCESSOR_GETTER => 'getter'; | |||||
| 11 | Readonly my $ACCESSOR_SETTER => 'setter'; | |||||
| 12 | Readonly my $ACCESSOR_GETSET => 'getset'; | |||||
| 13 | ||||||
| 14 | # -------------------------------------------------- | |||||
| 15 | # Output type strings from the schema | |||||
| 16 | # -------------------------------------------------- | |||||
| 17 | Readonly my $TYPE_BOOLEAN => 'boolean'; | |||||
| 18 | Readonly my $TYPE_OBJECT => 'object'; | |||||
| 19 | Readonly my $TYPE_VOID => 'void'; | |||||
| 20 | ||||||
| 21 | # -------------------------------------------------- | |||||
| 22 | # Default confidence threshold for plan generation | |||||
| 23 | # -------------------------------------------------- | |||||
| 24 | Readonly my $DEFAULT_CONFIDENCE => 'medium'; | |||||
| 25 | ||||||
| 26 | # -------------------------------------------------- | |||||
| 27 | # Test plan flag keys written to the method plan | |||||
| 28 | # -------------------------------------------------- | |||||
| 29 | Readonly my $TEST_CONTEXT => 'context_tests'; | |||||
| 30 | Readonly my $TEST_PREDICATE => 'predicate_test'; | |||||
| 31 | Readonly my $TEST_GETTER => 'getter_test'; | |||||
| 32 | Readonly my $TEST_SETTER => 'setter_test'; | |||||
| 33 | Readonly my $TEST_GETSET => 'getset_test'; | |||||
| 34 | Readonly my $TEST_OBJECT_INJECT => 'object_injection_test'; | |||||
| 35 | Readonly my $TEST_BOOLEAN_SET => 'boolean_set_test'; | |||||
| 36 | Readonly my $TEST_VOID => 'void_context_test'; | |||||
| 37 | Readonly my $TEST_ERROR_HANDLING => 'error_handling_test'; | |||||
| 38 | Readonly my $TEST_BOUNDARY => 'boundary_tests'; | |||||
| 39 | Readonly my $TEST_CHAINING => 'chaining_test'; | |||||
| 40 | Readonly my $TEST_BASIC => 'basic_test'; | |||||
| 41 | ||||||
| 42 | our $VERSION = '0.41'; | |||||
| 43 | ||||||
| 44 - 102 | =head1 VERSION
Version 0.41
=head1 DESCRIPTION
Generates a test strategy plan for all methods in a schema, determining
which test types should be produced for each method based on its
accessor classification, output type, and other metadata. Side-effect
and dependency-driven planning (mocking, isolation) is handled
separately by L<App::Test::Generator::Planner::Mock> and
L<App::Test::Generator::Planner::Isolation>.
=head2 new
Construct a new TestStrategy.
my $strategy = App::Test::Generator::TestStrategy->new(
schema => \%schemas,
thresholds => { confidence => 'high' },
);
=head3 Arguments
=over 4
=item * C<schema>
A hashref of method name to schema hashref. Optional â defaults to
an empty hashref.
=item * C<thresholds>
A hashref of threshold configuration. Optional â defaults to
C<< { confidence => 'medium' } >>.
=back
=head3 Returns
A blessed hashref.
=head3 API specification
=head4 input
{
schema => { type => HASHREF, optional => 1 },
thresholds => { type => HASHREF, optional => 1 },
}
=head4 output
{
type => OBJECT,
isa => 'App::Test::Generator::TestStrategy',
}
=cut | |||||
| 103 | ||||||
| 104 | sub new { | |||||
| 105 | 26 | 113489 | my ($class, %args) = @_; | |||
| 106 | return bless { | |||||
| 107 | schema => $args{schema} || {}, | |||||
| 108 | 26 | 109 | thresholds => $args{thresholds} || { confidence => $DEFAULT_CONFIDENCE }, | |||
| 109 | plans => {}, | |||||
| 110 | }, $class; | |||||
| 111 | } | |||||
| 112 | ||||||
| 113 - 153 | =head2 generate_plan
Generate a test plan for all methods in the schema and return it as
a hashref mapping method names to plan hashrefs.
my $strategy = App::Test::Generator::TestStrategy->new(
schema => \%schemas,
);
my $plan = $strategy->generate_plan;
for my $method (keys %{$plan}) {
print "$method: ", join(', ', keys %{ $plan->{$method} }), "\n";
}
=head3 Arguments
None beyond C<$self>.
=head3 Returns
A hashref mapping method names to test plan hashrefs, each containing
boolean flags for the test types that should be generated.
=head3 API specification
=head4 input
{
self => { type => OBJECT, isa => 'App::Test::Generator::TestStrategy' },
}
=head4 output
{
type => HASHREF,
keys => {
'*' => { type => HASHREF },
},
}
=cut | |||||
| 154 | ||||||
| 155 | sub generate_plan { | |||||
| 156 | 22 | 298 | my $self = $_[0]; | |||
| 157 | ||||||
| 158 | 22 22 | 16 41 | for my $method (keys %{ $self->{schema} }) { | |||
| 159 | 22 | 21 | my $schema = $self->{schema}{$method}; | |||
| 160 | ||||||
| 161 | # Generate and store the plan for this method | |||||
| 162 | 22 | 33 | $self->{plans}{$method} = $self->_plan_for_method($schema); | |||
| 163 | } | |||||
| 164 | ||||||
| 165 | 21 | 25 | return $self->{plans}; | |||
| 166 | } | |||||
| 167 | ||||||
| 168 | # -------------------------------------------------- | |||||
| 169 | # _plan_for_method | |||||
| 170 | # | |||||
| 171 | # Determine which test types should be | |||||
| 172 | # generated for a single method based on | |||||
| 173 | # its schema metadata. | |||||
| 174 | # | |||||
| 175 | # Entry: $schema - the per-method schema hashref | |||||
| 176 | # | |||||
| 177 | # Exit: Returns a hashref of test type flags. | |||||
| 178 | # Always contains at least basic_test => 1. | |||||
| 179 | # | |||||
| 180 | # Side effects: None. | |||||
| 181 | # | |||||
| 182 | # Notes: All string comparisons use // '' guards | |||||
| 183 | # to avoid uninitialized value warnings | |||||
| 184 | # when schema fields are absent. | |||||
| 185 | # -------------------------------------------------- | |||||
| 186 | sub _plan_for_method { | |||||
| 187 | 23 | 29 | my ($self, $schema) = @_; | |||
| 188 | ||||||
| 189 | 23 | 14 | my %plan; | |||
| 190 | ||||||
| 191 | # -------------------------------------------------- | |||||
| 192 | # Context-aware returns need both scalar and list | |||||
| 193 | # context tests to verify correct behaviour in each | |||||
| 194 | # -------------------------------------------------- | |||||
| 195 | 23 | 37 | if($schema->{output}{_context_aware}) { | |||
| 196 | 1 | 1 | $plan{$TEST_CONTEXT} = 1; | |||
| 197 | } | |||||
| 198 | ||||||
| 199 | # -------------------------------------------------- | |||||
| 200 | # Accessor detection â choose test types based on | |||||
| 201 | # whether the method is a getter, setter, or both | |||||
| 202 | # -------------------------------------------------- | |||||
| 203 | 22 10 | 44 19 | if($schema->{accessor} && scalar keys %{ $schema->{accessor} }) { | |||
| 204 | 10 | 16 | my $acc_type = $schema->{accessor}{type} // ''; | |||
| 205 | ||||||
| 206 | 10 | 17 | if($acc_type eq $ACCESSOR_GETTER) { | |||
| 207 | # Boolean getters are predicates and need | |||||
| 208 | # truthy/falsy tests in addition to getter tests | |||||
| 209 | 3 | 18 | if(($schema->{output}{type} // '') eq $TYPE_BOOLEAN) { | |||
| 210 | 1 | 3 | $plan{$TEST_PREDICATE} = 1; | |||
| 211 | } | |||||
| 212 | 3 | 14 | $plan{$TEST_GETTER} = 1; | |||
| 213 | ||||||
| 214 | } elsif($acc_type eq $ACCESSOR_SETTER) { | |||||
| 215 | 1 | 6 | $plan{$TEST_SETTER} = 1; | |||
| 216 | ||||||
| 217 | } elsif($acc_type eq $ACCESSOR_GETSET) { | |||||
| 218 | # For getset accessors, check the input parameter | |||||
| 219 | # type to determine if object injection or boolean | |||||
| 220 | # set tests are more appropriate. Sort by position (keys | |||||
| 221 | # %hash has no defined order) so the choice is deterministic | |||||
| 222 | # if more than one candidate is present. | |||||
| 223 | 6 | 42 | my $input = $schema->{input} || {}; | |||
| 224 | my ($param) = sort { | |||||
| 225 | 2 | 7 | ($input->{$a}{position} // 9999) <=> ($input->{$b}{position} // 9999) | |||
| 226 | || $a cmp $b | |||||
| 227 | 6 9 6 | 4 16 6 | } grep { !/^_/ } keys %{ $input }; | |||
| 228 | 6 | 15 | my $param_type = ($param && $input->{$param}{type}) // ''; | |||
| 229 | ||||||
| 230 | 6 | 8 | if($param_type eq $TYPE_OBJECT) { | |||
| 231 | 1 | 2 | $plan{$TEST_OBJECT_INJECT} = 1; | |||
| 232 | } elsif($param_type eq $TYPE_BOOLEAN) { | |||||
| 233 | 4 | 19 | $plan{$TEST_BOOLEAN_SET} = 1; | |||
| 234 | } | |||||
| 235 | 6 | 20 | $plan{$TEST_GETSET} = 1; | |||
| 236 | } | |||||
| 237 | } | |||||
| 238 | ||||||
| 239 | # -------------------------------------------------- | |||||
| 240 | # Void return type â verify the method returns nothing | |||||
| 241 | # and does not accidentally return a useful value | |||||
| 242 | # -------------------------------------------------- | |||||
| 243 | 22 | 61 | if(($schema->{output}{type} // '') eq $TYPE_VOID) { | |||
| 244 | 2 | 8 | $plan{$TEST_VOID} = 1; | |||
| 245 | } | |||||
| 246 | ||||||
| 247 | # -------------------------------------------------- | |||||
| 248 | # Error handling â verify error return conventions | |||||
| 249 | # are tested explicitly | |||||
| 250 | # -------------------------------------------------- | |||||
| 251 | 22 | 88 | if($schema->{output}{_error_return} | |||
| 252 | || $schema->{output}{success_failure_pattern}) { | |||||
| 253 | 2 | 2 | $plan{$TEST_ERROR_HANDLING} = 1; | |||
| 254 | } | |||||
| 255 | ||||||
| 256 | # -------------------------------------------------- | |||||
| 257 | # Boundary hints from YAML test configuration â | |||||
| 258 | # generate boundary/equivalence class tests | |||||
| 259 | # -------------------------------------------------- | |||||
| 260 | 22 1 | 30 8 | if($schema->{_yamltest_hints} && keys %{ $schema->{_yamltest_hints} }) { | |||
| 261 | 1 | 2 | $plan{$TEST_BOUNDARY} = 1; | |||
| 262 | } | |||||
| 263 | ||||||
| 264 | # -------------------------------------------------- | |||||
| 265 | # Method chaining â verify that $self is returned | |||||
| 266 | # and that calls can be chained | |||||
| 267 | # -------------------------------------------------- | |||||
| 268 | 22 | 28 | if($schema->{output}{_returns_self}) { | |||
| 269 | 1 | 1 | $plan{$TEST_CHAINING} = 1; | |||
| 270 | } | |||||
| 271 | ||||||
| 272 | # -------------------------------------------------- | |||||
| 273 | # Boolean output â needs predicate tests regardless | |||||
| 274 | # of whether an accessor was detected | |||||
| 275 | # -------------------------------------------------- | |||||
| 276 | 22 | 42 | if(($schema->{output}{type} // '') eq $TYPE_BOOLEAN) { | |||
| 277 | 2 | 5 | $plan{$TEST_PREDICATE} = 1; | |||
| 278 | } | |||||
| 279 | ||||||
| 280 | # -------------------------------------------------- | |||||
| 281 | # Always generate at least a basic call test even | |||||
| 282 | # if no other test types were identified | |||||
| 283 | # -------------------------------------------------- | |||||
| 284 | 22 | 61 | $plan{$TEST_BASIC} = 1 unless %plan; | |||
| 285 | ||||||
| 286 | 22 | 41 | return \%plan; | |||
| 287 | } | |||||
| 288 | ||||||
| 289 | 1; | |||||