Test-More / test-more

Test2, Test::More, Test::Simple and Test::Builder Perl modules for writing tests
Other
140 stars 89 forks source link

Normalizing the DSL #979

Open schwern opened 8 years ago

schwern commented 8 years ago

The idea here is to reduce the number of different interface ideas a Test2 user has to deal with, reduce the tendency to create ever increasingly specific functions, increase the flexibility of the functions provided by Test2, and let tests be easy to understand at a glance.

I'd split the descriptive interface (ie. what we use to build up checks) into three major pieces.

These do three things.

  1. Verify the thing is of that type.
  2. Provide an optional block for more descriptions.
  3. Provide context for the enclosed descriptions.

3 allows us to reuse the same function names for the same context in different contexts. I'll get to that below.

Type descriptions provided by Test2 are a fairly small set constrained by Perl itself.

They can be used alone.

is $thing, number;

Or with a block of further descriptions.

is $thing, number { positive; integer; };

The types don't need to get overly specific because extra restrictions can be added by the enclosed content descriptions.

3rd party modules can provide their own type descriptions. For example, maybe one for testing XML and HTML.

is $xml, xml { xpath "//items/item#$id" };
is $html, html { xpath "//div#$id" };

Content Descriptions

These are all the various ways people might want to describe basic Perl structures. empty, match, blank, fields, items, etc... They basically work like they do now except they're aware of their type context. This allows us to use the same function describing the same context for different types. For example.

is "", string { empty; };
is [], array { empty };

Taking a page from PHP (hey, they got something right) we could merge field and item into just field and fields.

is [23, 42], array { field 0 => 23; field 1 => 42 };
is [23, 42], array { field 23; field 42; };
is [23, 42], array { fields 23, 42 };
is { foo => 23, bar => 42 }, hash { field foo => 23; field bar => 42 };
is { foo => 23, bar => 42 }, hash { fields foo => 23, bar => 42 };

This could be implemented internally by having the checks work on a Value object rather than the bare value itself. is would pass a Value::Any object to string. Once string verifies the contained value is a string, it changes it to a Value::String object and calls the empty method on it.

If it doesn't have such a method, the check fails.

is "   ", string { blank; }; # pass
is [], array { blank; };  # fail, not an error

Content descriptions can be used outside a type block in which case they work on Value::Any and may have an implied type check. This provides a nice short hand for simple scalar descriptions.

is "   ", blank; # implies string { blank }
is 23, true;

Composition

This also allows users to add multiple descriptions of the same thing.

is $number, number { positive; integer; };  # 3 passes, 3.1 fails.

This avoids having to predict and litter the interface with composition functions like "positive_integer", "positive_decimal", "negative_integer", and "negative_decimal".

Nesting

Descriptions can be nested, as they can be right now.

is $thing, array {
    fields 23, 42, number { positive; integer; }
};

Repeatable descriptions

Descriptions could be stored to be used later. This would both be useful to provide even more descriptive interface (a la petdance) or for quick reuse in a test script.

my $pos_int = number { positive; integer; };
is $thing, hash {
    fields foo => $pos_int, bar => $pos_int
};

Conclusion

I think this provides a very good balance between...

exodist commented 8 years ago

Please note that string() and number() do not currently take blocks, and if we make blocks optional we break the prototypes of the existing checks.

Using the 'pin' feature of Importer.pm (still in beta) we can still move forward without breaking things, and change these, but be advised that we will have to deal with prototypes (or killing them).

schwern commented 8 years ago

Please note that string() and number() do not currently take blocks, and if we make blocks optional we break the prototypes of the existing checks.

I'd like to keep this discussion at the level of what we want and then worry about implementation and compat details later. It's good to agree where you're going before you try to get there.

schwern commented 8 years ago

(As I update the spec I'll also add the updates as comments)

Content descriptions can be used outside a type block in which case they work on Value::Any and perhaps have an implied appropriate type check. This provides a nice short hand for simple scalar descriptions.

is "   ", blank;
is 23, true;
schwern commented 8 years ago

Nick Fagerlund had a glance at this and suggested we look at Puppet's value and data types for inspiration.