NAME
Test2::Tools::TypeTiny - Test2 tools for checking Type::Tiny types
VERSION
version v0.93.0
SYNOPSIS
use Test2::V0;
use Test2::Tools::TypeTiny;
use MyTypes qw< FullyQualifiedDomainName >;
type_subtest FullyQualifiedDomainName, sub {
my $type = shift;
should_pass_initially(
$type,
qw<
www.example.com
example.com
www123.prod.some.domain.example.com
llanfairpwllgwyngllgogerychwyrndrobwllllantysiliogogogoch.co.uk
>,
);
should_fail(
$type,
qw< www ftp001 .com domains.t x.c prod|ask|me -prod3.example.com >,
);
should_coerce_into(
$type,
qw<
ftp001-prod3 ftp001-prod3.ourdomain.com
prod-ask-me prod-ask-me.ourdomain.com
nonprod3-foobar-me nonprod3-foobar-me.ourdomain.com
>,
);
should_sort_into(
$type,
[qw< ftp001-prod3 ftp001-prod3.ourdomain.com prod-ask-me.ourdomain.com >],
);
parameters_should_create_type(
$type,
[], [3], [0, 0], [1, 2],
);
parameters_should_die_as(
$type,
[], qr<Parameter for .+ does not exist>,
[-3], qr<Parameter for .+ is not a positive int>,
[0.2], qr<Parameter for .+ is not a positive int>,
);
message_should_report_as(
$type,
undef, qr<Must be a valid FQDN>
);
explanation_should_report_as(
$type,
undef, [
qr<Undef did not pass type constraint>,
],
);
};
done_testing;
DESCRIPTION
This module provides a set of tools for checking Type::Tiny types. This is similar to Test::TypeTiny, but works against the Test2::Suite and has more functionality for testing and troubleshooting coercions, error messages, and other aspects of the type.
FUNCTIONS
All functions are exported by default. These functions create buffered subtests to contain different classes of tests.
Besides the wrapper itself, these functions are most useful wrapped inside of a "type_subtest" coderef.
Wrappers
type_subtest
type_subtest Type, sub {
my $type = shift;
...
};
Creates a subtest with the given type as the test name, and passed as the only parameter. Using a generic $type
variable makes it much easier to copy and paste test code from other type tests without accidentally forgetting to change your custom type within the code.
If the type can be inlined, this will also run two separate subtests (within the main type subtest) to check both the inlined constraint and the slower coderef constraint. The second subtest will have a inline-less type, cloned from the original type. This is done by stripping out the inlined constraint (or generator) in the clone.
The tester sub will be used in both subtests. If you need the inlined constraint for certain tests, you can use the $type->can_be_inlined
method to check which version of the test its running. However, inlined checks should do the exact same thing as coderef checks, so keep these kind of exceptions to a minimum.
Note that it doesn't do anything to the parent types. If your type check is solely relying on parent checks, this will only run the one subtest. If the parent checks are part of your package, you should check those separately.
Value Testers
Most of these checks will run through get_message
and validate_explain
calls to confirm the coderefs don't die. If you need to validate the error messages themselves, consider using some of the "Error Message Testers".
should_pass_initially
should_pass_initially($type, @values);
Creates a subtest that confirms the type will pass with all of the given @values
, without any need for coercions.
should_fail_initially
should_fail_initially($type, @values);
Creates a subtest that confirms the type will fail with all of the given @values
, without using any coercions.
This function is included for completeness. However, items in should_fail_initially
should realistically end up in either a "should_fail" block (if it always fails, even with coercions) or a "should_coerce_into" block (if it would pass after coercions).
should_pass
should_pass($type, @values);
Creates a subtest that confirms the type will pass with all of the given @values
, including values that might need coercions. If it initially passes, that's okay, too. If the type does not have a coercion and it fails the initial check, it will stop there and fail the test.
This function is included for completeness. However, "should_coerce_into" is the better function for types with known coercions, as it checks the resulting coerced values as well.
should_fail
should_fail($type, @values);
Creates a subtest that confirms the type will fail with all of the given @values
, even when those values are ran through its coercions.
should_coerce_into
should_coerce_into($type, @orig_coerced_kv_pairs);
should_coerce_into($type,
# orig # coerced
undef, 0,
[], 0,
);
Creates a subtest that confirms the type will take the "key" in @orig_coerced_kv_pairs
and coerce it into the "value" in @orig_coerced_kv_pairs
. (The @orig_coerced_kv_pairs
parameter is essentially an ordered hash here, with support for ref values as the "key".)
The original value should not pass initial checks, as it would not be coerced in most use cases. These would be considered test failures.
Parameter Testers
These tests should only be used for parameter validation. None of the resulting types are checked in other ways, so you should include other type subtests with different kinds of parameterized types.
Note that inline generators don't require any sort of validation because the constraint generator is always called first, and should die on parameter validation failure, prior to the inline_generator
call. The same applies for coercion generators as well.
parameters_should_create_type
parameters_should_create_type($type, @parameter_sets);
parameters_should_create_type($type,
[],
[3],
[0, 0],
[1, 2],
);
Creates a subtest that confirms the type will successfully create a parameterized type with each of the set of parameters in @parameter_sets
(a list of arrayrefs).
parameters_should_die_as
parameters_should_die_as($type, @parameter_sets_exception_regex_pairs);
parameters_should_die_as($type,
# params # exceptions
[], qr<Parameter for .+ does not exist>,
[-3], qr<Parameter for .+ is not a positive int>,
[0.2], qr<Parameter for .+ is not a positive int>,
);
Creates a subtest that confirms the type will fail validation (fatally) with the given parameters and exceptions. The RHS should be an regular expression, but can be anything that like accepts.
Error Message Testers
message_should_report_as
message_should_report_as($type, @value_message_regex_pairs);
message_should_report_as($type,
# values # messages
1, qr<Must be a fully-qualified domain name, not 1>,
undef, qr!Must be a fully-qualified domain name, not <undef>!,
# valid value; checking message, anyway
'example.com', qr<Must be a fully-qualified domain name, not example.com>,
);
Creates a subtest that confirms error message output against the value. Technically, "get_message" in Type::Tiny works for valid values, too, so this isn't actually trapping assertion failures, just checking the output of that method.
The RHS should be an regular expression, but it can be anything that like accepts.
explanation_should_report_as
explanation_should_report_as($type, @value_explanation_check_pairs);
explanation_should_report_as($type,
# values # explanation check
'example.com', [
qr< did not pass type constraint >,
qr< expects domain label count \(\?LD\) to be between 3 and 5>,
qr<\$_ appears to be a 2LD>,
],
undef, [
qr< did not pass type constraint >,
qr<\$_ is not a legal FQDN>,
],
);
Creates a subtest that confirms deeper explanation message output from "validate_explain" in Type::Tiny against the value. Unlike get_message
, validate_explain
actually needs failed values to report back a string message. The second parameter to validate_explain
is not passed, so expect error messages that inspect $_
.
The RHS should be an arrayref of regular expressions, since validate_explain
reports back an arrayref of strings. Although, it can be anything that like accepts, and since it's a looser check, gaps in the arrayref are allowed.
Other Testers
should_sort_into
should_sort_into($type, @sorted_arrayrefs);
Creates a subtest that confirms the type will sort into the expected lists given. The input list is a shuffled version of the sorted list.
Because this introduces some non-deterministic behavior to the test, it will run through 100 cycles of shuffling and sorting to confirm the results. A good sorter should always return a deterministic result for a given list, with enough fallbacks to account for every unique case. Any failure will immediate stop the loop and return both the shuffled input and output list in the failure output, so that you can temporarily test in a more deterministic manner, as you debug the fault.
TROUBLESHOOTING
Test name output
The test names within each should_*
function are somewhat dynamic, depending on which stage of the test it failed at. Most of the time, this is self-explanatory, but double negatives may make the output a tad logic-twisting:
not ok 1 - ...
# should_*_initially
"val" should pass # simple should_pass_initially failure
"val" should fail # simple should_fail_initially failure
# should_*
"val" should fail (initial check) # should_fail didn't initially fail
"val" should pass (no coercion) # should_pass initally failed, and didn't have a coercion to use
"val" should pass (failed coercion) # should_pass failed both the check and coercion
"val" should fail (coerced into "val2") # should_fail still successfully coerced into a good value
"val" should pass (coerced into "val2") # should_pass coerced into a bad value
# should_coerce_into has similar errors as above
Type Map Diagnostics
Because types can be twisty mazes of inherited parents or multiple coercion maps, any failures will produce a verbose set of diagnostics. These come in two flavors: constraint maps and coercion maps, depending on where in the process the test failed.
For example, a constraint map could look like:
# (some definition output truncated)
MyStringType constraint map:
MyStringType->check("value") ==> FAILED
is defined as: do { package Type::Tiny; ... ) }
StrMatch["(?^ux:...)"]->check("value") ==> FAILED
is defined as: do { package Type::Tiny; !ref($_) and !!( $_ =~ $Types::Standard::StrMatch::expressions{"..."} ) }
StrMatch->check("value") ==> PASSED
is defined as: do { package Type::Tiny; defined($_) and do { ref(\$_) eq 'SCALAR' or ref(\(my $val = $_)) eq 'SCALAR' } }
Str->check("value") ==> PASSED
is defined as: do { package Type::Tiny; defined($_) and do { ref(\$_) eq 'SCALAR' or ref(\(my $val = $_)) eq 'SCALAR' } }
Value->check("value") ==> PASSED
is defined as: (defined($_) and not ref($_))
Defined->check("value") ==> PASSED
is defined as: (defined($_))
Item->check("value") ==> PASSED
is defined as: (!!1)
Any->check("value") ==> PASSED
is defined as: (!!1)
The diagnostics checked the final value with each individual parent check (including itself). Based on this output, the value passed all of the lower-level Str
checks, because it is a string. But, it failed the more-specific StrMatch
regular expression. This will give you an idea of which type to adjust, if necessary.
A coercion map would look like this:
MyStringType coercion map:
MyStringType->check("value") ==> FAILED
FQDN->check("value") ==> FAILED
Username->check("value") ==> FAILED
Hostname->check("value") ==> PASSED (coerced into "value2")
The diagnostics looked at Type::Coercion's type_coercion_map
(and the type itself), figured out which types were acceptable for coercion, and returned the coercion result that passed. In this case, none of the types passed except Hostname
, which was coerced into value2
.
Based on this, either Hostname
converted it to the wrong value (one that did not pass MyStringType
), or one of the higher-level checks should have passed and didn't.
AUTHOR
Grant Street Group <developers@grantstreet.com>
COPYRIGHT AND LICENSE
This software is Copyright (c) 2024 - 2025 by Grant Street Group.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)