|
NAMETest::Inter - framework for more readable interactive test scripts DESCRIPTIONThis is another framework for writing test scripts. Much of the syntax is loosely inspired by Test::More, and Test::Inter has most of it's functionality, but it is not a drop-in replacement. Test::More (and other existing test frameworks) suffer from two weaknesses, both of which have prevented me from ever using them: None offer the ability to access specific tests in a reasonably interactive fashion, primarily for debugging purposes None offer the ability to write the tests in whatever format would make the tests the most readable The way I write and use test scripts, existing Test::* modules are not nearly as useful as they could be. Test scripts written using Test::More work fine when running as part of the test suite, but debugging an individual test requires extra steps, and the tests themselves are not as readable as they should be. INTERACTIVE EXECUTIONOne requirement that I have of a test framework is the ability to interact with it. I do most of my debugging using test scripts. When I find a bug, I write a test case for it (typically by adding it to an existing test script) and then debug it using the test script. Then I leave the test there to ensure that the bug won't come back (hopefully). Since I use test scripts in a very interactive way (often in the debugger), I want to be able to do the following trivially:
To illustrate the first point, in Test::More, a series of tests might be specified in a test script as shown in the following example (line numbers added for convenience): ...
100: # test 1
101: $result = func("apples","bushels");
102: is($result, "enough");
103:
104: # test 2
105: $result = func("grapefruit","tons");
106: is($result, "enough");
107:
108: # test 3
109: $result = func("oranges","boatloads");
110: is($result, "insufficient");
111:
112: # tests 4-6
113: foreach my $arg (qw(pears plums pineapple)) {
114: $result = func($arg,"boxes");
115: is($result, "enough");
116: }
...
Say you ran the test suite, and test 3 failed. To debug it you have to open up the test script, find the 3rd test, and set the appropriate breakpoint. In this case, you'll want to break at line 109. None of these steps are impossible of course, but it will take some time to get it right. It becomes harder when there are lots of tests (imagine that you want to test the 117th test instead of the 3rd test) or when tests are wrapped up in loops, embedded in subroutines, or other similar situations. As an example, what if it's the 5th test that fails in the example above. Now the break point will be a conditional one, so you have to figure out not only the line, but the condition the appropriate state during that test. In this case, you need to stop at line 114 when $arg is 'plums'. Wouldn't it be far better to set a break point in func when the Nth test is reached? With Test::Inter, you can. So for the above script, the debugger commands that you would use to debug the 3rd test are: Test::More : b 109 Test::Inter: b func ($::TI_NUM==3) and the 5th test are: Test::More : b 114 ($arg eq 'plums') Test::Inter: b func ($::TI_NUM==5) It would also be nice to be able to skip the first two tests... perhaps they take a long time to run, and I want to get right to work on test 3. You can do this easily too by setting the $::TI_START variable. There are some other variables that can be used to specify which test or tests to run described in the "TEST::INTER VARIABLES" section below. The other thing I want to do when I run the test scripts interactively is to see more information which will assist in debugging a failed test. This can be controlled with variables such as TI_QUIET, TI_MODE, and TI_WIDTH described below in the "TEST::INTER VARIABLES" section. READABLE TESTSThe other feature that I wanted in a test suite is the ability to define the tests in a format that is natural and readable FOR THE TESTS. In almost every case, it is best to think of a test script as consisting of two separate parts: a script part, and a test part. The script part of a test script is the least important part! It's usually fairly trivial, rarely needs to be changed, and is not the focus of the test script. The tests part of the script IS the important part, and these should be expressed in a form that is natural to them, easy to maintain, easy to read, and easy to modify, and none of these should involve modifying the script portion of the test script in general. Because the content of the tests is the important part of the script, the emphasis should be in making them more readable, even at the expense of the script portion. As a general rule, if the script portion of the test script obscures the tests in any way, it's not written correctly! The solution to this is well understood, and is common to many other systems where you are mixing two "languages". The task of correctly specifying both the tests and the test script is virtually identical to the task of creating a PHP script which consists of a mixture of PHP and HTML, or the task of creating a template file using some templating system where the file consists of a mixture of text to be displayed and templating commands. It is well understood in each of these cases that the more the two "languages" are interwoven, the less readable both are, and the harder it is to maintain. The more you are able to separate the two, the easier both are to read and maintain. As often as possible, I want the tests to be written in some sort of text format which can be easily viewed and modified (usually as a simple table) with no perl commands interspersed. I want to the freedom to define the tests in one section (a long string, the DATA section, or even in a separate file) which is easily readable. This may introduce the necessity of parsing it, but it makes it significantly easier to maintain the tests. This flexibility makes it much easier to read the tests (as opposed to the script) which is the fundamental content of a test script. Looking again at the example test script, you can see that there is far too much perl interspersed with the tests. It's difficult to read the tests individually in this script because there is too much perl code among them, and virtually impossible to look at them as a whole. It is true that looking at this particular example, it is very simple... but the script ISN'T the content you're interested in (and bear in mind that many test scripts are nowhere near this simple). The REAL content of this script are the tests, which consist of the function arguments and the expected result. Although it's not impossible to see each of these in the script above, it's not in a format that is conducive to studying the tests, and especially not for examining the list of tests as a whole. Now, look at an alternate way of specifying the tests using this module: $tests = "
apples bushels => enough
grapefruit tons => enough
oranges boatloads => insufficient
pears boxes => enough
plums boxes => enough
pineapple boxes => enough
";
$o->tests(tests => $tests,
func => \&func);
Here, it's easy to see the list of tests, and adding additional tests is a breeze. CREATING A TESTThis module supports a number of methods for defining tests, so you can use whichever one is most convenient (including methods that are identical to Test::More if that really is the best method). Every test may have several pieces of information:
BASE METHODS
METHODS FOR LOADING MODULESTest scripts can load other modules (using either the perl "use" or "require" commands). There are three different modes for doing this which determine how this is done.
The methods available are:
METHODS FOR RUNNING TESTThere are several methods for running tests. The ok, is, and isnt methods are included for those already comfortable with Test::More and wishing to stick with the same format of test script. The tests method is the suggested method though since it makes use of the full power of this module.
USING THE TESTS METHODIt is expected that most tests (except for those that load a module) will be run using the tests method called as: $o->tests(%options); The following options are available:
SPECIFYING THE TESTSA series of tests can be specified in two different ways. The tests can be written in a very simple string format, or stored as a list. Demonstrating how this can be done is best done by example, so let's say that there is a function (func) which takes two arguments, and returns a single value. Let's say that the expected output (and the actual output) from 3 different sets of arguments is: Input Expected Output Actual Output ----- --------------- ------------- 1,2 a a 3,4 b x 5,6 c c (so in this case, the first and third tests pass, but the 2nd one will fail). Specifying these tests as lists could be done as: $o->tests(
func => &func,
tests => [ [1,2], [3,4], [5,6] ],
expected => [ [a], [b], [c] ],
);
Here, the tests are stored as a list, and each element in the list is a listref containing the set of arguments. If the func option is not passed in, the tests option is set to a list of results to compare with the expected results, so the following is equivalent to the above: $o->tests(
tests => [ [a], [x], [c] ],
expected => [ [a], [b], [c] ],
);
If an argument (or actual result) or an expected result is only a single value, it can be entered as a scalar instead of a list ref, so the following is also equivalent: $o->tests(
func => &func,
tests => [ [1,2], [3,4], [5,6] ],
expected => [ a, b, [c] ],
);
The only exception to this is if the single value is itself a list reference. In this case it MUST be included as a reference. In other words, if you have a single test, and the expected value for this test is a list reference, it must be passed in as: expected => [ [ \@r ] ] NOT as: expected => [ \@r ] Passing in a set of expected results is optional. If none are passed in, the tests are treated as if they had been passed to the ok method (i.e. if they return something true, they pass, otherwise they fail). The second way to specify tests is as a string. The string is a multi-line string with each tests being separate from the next test by a blank line. Comments (lines which begin with '#') are allowed, and are ignored. Whitespace at the start and end of the line is ignored. The string may contain the results directly, or results may be passed in separately. For example, the following all give the same sets of tests as the example above: $o->tests(
func => &func,
tests => "
# Test 1
1 2 => a
# Test 2
3 4 => b
5 6 => c
",
);
$o->tests(
func => &func,
tests => "
1 2
3 4
5 6
",
expected => [ [a], [b], [c] ]
);
$o->tests(
func => &func,
tests => [ [1,2], [3,4], [5,6] ],
expected => "
a
b
c
",
);
$o->tests(
func => &func,
tests => "
1 2
3 4
5 6
",
expected => "
a
b
c
",
);
The expected results may also consist of only a single set of results (in this case, it must be passed in as a listref). In this case, all of the tests are expected to have the same results. So, the following are equivalent: $o->tests(
func => &func,
tests => "
1 2 => a b
3 4 => a b
5 6 => a b
",
);
$o->tests(
func => &func,
tests => "
1 2
3 4
5 6
",
expected => [ [a, b] ],
);
$o->tests(
func => &func,
tests => "
1 2
3 4
5 6
",
expected => "a b",
);
The number of expected values must either be 1 (i.e. all of the tests are expected to produce the same value) or exactly the same number as the number of tests. The parser is actually quite powerful, and can handle multi-line tests, quoted strings, and nested data structures. The test may be split across any number of lines, provided there is not a completely blank line (which signals the end of the test), so the following are equivalent: tests => "a b c",
tests => "a b
c",
Arguments (or expected results) may include data structures. For example, the following are equivalent: tests => "[ a b ] { a 1 b 2 }"
tests => [ [ [a,b], { a=>1, b=>2 } ] ]
Whitespace is mostly optional, but there is one exception. An item must end with some kind of delimiter, so the following will fail: tests => "[a b][c d]" The first element (the list ref [a b]) must be separated from the second element by the delimiter (which is whitespace in this case), so it must be written as: tests => "[a b] [c d]" As already demonstrated, hashrefs and listrefs may be included and nested. Elements may also be included inside parens, but this is optional since all arguments and expected results are already treated as lists, so the following are equivalent: tests => "a b c" tests => "(a b) c" Although parens are optional, they may make things more readable, and allow you to use something other than whitespace as the delimiter. Since parens are actually ignored, a string '()' is also ignored, so do not use empty parentheses. If the character immediately following the opening paren, brace, or bracket is a punctuation mark, then it is used as the delimiter instead of whitespace. For example, the following are all equivalent: [ a b c ] [a b c] [, a,b,c ] [, a, b, c ] A delimiter is a single character, and the following may not be used as a delimiter: any opening/closing characters () [] {}
single or double quotes
alphanumeric characters
underscore
Whitespace (including newlines) around the delimiter is ignored, so the following is valid: [, a,
b,
c ]
Two delimiters next to each other or a trailing delimiter produce an empty string. "(,a,b,)" => (a, b, '') "(,a,,b)" => (a, '', b) Hashrefs may be specified by braces and the following are equivalent: { a 1 b 2 }
{, a,1,b,2 }
{, a,1,b,2, }
Note that a trailing delimiter is ignored if there are already an even number of elements, or an empty string otherwise. Nested structures are allowed: "[ [1 2] [3 4] ]" For example, $o->tests(
func => &func,
tests => "a [ b c ] { d 1 e 2 } => x y"
);
is equivalent to: $o->tests(
func => &func,
tests => [ [a, [b,c], {d=>1,e=>2}] ],
results => [ [x,y] ],
);
Any single value can be surrounded by single or double quotes in order to include the delimiter. So: "(, a,'b,c',e )" is equivalent to: "( a b,c e )" Any single value can be the string '__undef__' which will be turned into an actual undef. If the value is '__blank__' it is turned into an empty string (''), though it can also be specified as '' directly. Any value can have an embedded newline by including a __nl__ in the value, but the value must be written on a single line. Expected results are separated from arguments by ' => '. TEST::INTER VARIABLESTo summarize the information above, the following variables are used by Test::Inter. Each variable can be set in two different ways: as an environment variable and as a perl variable in the main namespace. For example, the TI_END variable can be set as: $::TI_END
$ENV{TI_END}
The following variables can be used to define which tests are run:
There is also a variable TI_NUM (available only as $::TI_NUM) which is set automatically by Test::Inter to be the test currently being run. The following variables control what is output from the tests, and how it is formatted:
The following variables control how some tests are run:
HISTORYThe history of this module dates back to 1996 when I needed to write a test suite for my Date::Manip module. At that time, none of the Test::* modules currently available in CPAN existed (the earliest ones didn't come along until 1998), so I was left completely on my own writing my test scripts. I wrote a very basic version of my test framework which allowed me to write all of the tests as a string, it would parse the string, count the tests, and then run them. Over the years, the functionality I wanted grew, and periodically, I'd go back and reexamine other Test frameworks (primarily Test::More) to see if I could replace my framework with an existing module... and I've always found them wanting, and chosen to extend my existing framework instead. As I've written other modules, I've wanted to use the framework in them too, so I've always just copied it in, but this is obviously tedious and error prone. I'm not sure why it took me so long... but in 2010, I finally decided it was time to rework the framework in a module form. I loosely based my module on Test::More. I like the functionality of that module, and wanted most of it (and I plan on adding more in future versions). So this module uses some similar syntax to Test::More (though it allows a great deal more flexibility in how the tests are specified). One thing to note is that I may have been able to write this module as an extension to Test::More, but after looking into that possibility, I decided that it would be faster to not do that. I did "borrow" a couple of routines from it (though they've been modified quite heavily) as a starting point for a few of the functions in this module, and I thank the authors of Test::More for their work. KNOWN BUGS AND LIMITATIONSNone known. SEE ALSOTest::More - the 'industry standard' of perl test frameworks BUGS AND QUESTIONSIf you find a bug in Test::Inter, there are three ways to send it to me. Any of them are fine, so use the method that is easiest for you.
Please do not use other means to report bugs (such as forums for a specific OS or Linux distribution) as it is impossible for me to keep up with all of them. When filing a bug report, please include the following information:
If you want to report missing or incorrect codes, you must be running the most recent version of Test::Inter. If you find any problems with the documentation (errors, typos, or items that are not clear), please send them to me. I welcome any suggestions that will allow me to improve the documentation. LICENSEThis script is free software; you can redistribute it and/or modify it under the same terms as Perl itself. AUTHORSullivan Beck (sbeck@cpan.org)
|