http://qs321.pair.com?node_id=11122132

Lady_Aleena has asked for the wisdom of the Perl Monks concerning the following question:

The test I needed help with in Can Test::MockObject mock a file? is finished. I would like to know how I did writing it.

I got some advice on looping I did not quite get a handle on, but this is a short test for a little module.

Here are links to the Fancy::Open module and Fancy::Open pod (unfinished).

#!perl use strict; use warnings; use v5.10.0; use Test::More tests => "14"; use File::Temp qw(tempfile); use Encode qw(encode); BEGIN { use_ok( 'Fancy::Open', qw(fancy_open) ) or die "Fancy::Open is not available\n"; } diag( "Testing Fancy::Open $Fancy::Open::VERSION, Perl $], $^X" ); my @wanted_array = qw(red orange yellow spring green teal cyan azu +re blue violet magenta pink white black gray); my @solid_array = map( "solid $_", @wanted_array ); my @bead_array = map( "$_ bead", @wanted_array ); my @solid_bead_array = map( "solid $_ bead", @wanted_array ); my $file_string = join( "\n", @wanted_array ); # Testing with file that ends with a newline my ($n_fh, $n_file) = tempfile(); $n_fh->print("$file_string\n"); $n_fh->close(); is_deeply( [ Fancy::Open::fancy_open($n_file) ], [ @wanted_array ], "testing a plain array with file that ends with a newline" ); is_deeply( [ Fancy::Open::fancy_open($n_file, { 'before' => 'solid ' }) ], [ @solid_array ], "testing an array with before option with file that ends with a newl +ine" ); is_deeply( [ Fancy::Open::fancy_open($n_file, { 'after' => ' bead' }) ], [ @bead_array ], "testing an array with after option with file that ends with a newli +ne" ); is_deeply( [ Fancy::Open::fancy_open($n_file, { 'before' => 'solid ', 'after' = +> ' bead' }) ], [ @solid_bead_array ], "testing an array with before and after options with file that ends +with a newline" ); # Testing with file that does not end with a newline my ($no_n_fh, $no_n_file) = tempfile(); $no_n_fh->print($file_string); $no_n_fh->close(); is_deeply( [ Fancy::Open::fancy_open($no_n_file) ], [ @wanted_array ], "testing a plain array with file that does not end with a newline" ); is_deeply( [ Fancy::Open::fancy_open($no_n_file, { 'before' => 'solid ' }) ], [ @solid_array ], "testing an array with before option with file that does not end wit +h a newline" ); is_deeply( [ Fancy::Open::fancy_open($no_n_file, { 'after' => ' bead' }) ], [ @bead_array ], "testing an array with after option with file that does not end with + a newline" ); is_deeply( [ Fancy::Open::fancy_open($no_n_file, { 'before' => 'solid ', 'after +' => ' bead' }) ], [ @solid_bead_array ], "testing an array with before and after options with file that does +not end with a newline" ); # Testing encoding my ($fh, $fn) = tempfile(); $fh->print($file_string); $fh->close(); is_deeply( [ Fancy::Open::fancy_open($fn) ], [ map encode('utf8', $_), @wanted_array ], "testing an array opened with no encoding" ); my @encodings = qw(UTF-8 ascii cp1252 iso-8859-1); for my $encoding (@encodings) { is_deeply( [ Fancy::Open::fancy_open($fn, { 'encoding' => $encoding }) ], [ map encode($encoding, $_), @wanted_array ], "testing an array opened with $encoding encoding" ); } done_testing(); # Written with the help of tobyink, davido, chromatic, and perlfan on +PerlMonks. # https://www.perlmonks.org/?node_id=11121949

My OS is Debian 10 (Buster); my perl versions are 5.28.1 local and 5.16.3 or 5.30.0 on web host depending on the shebang.

No matter how hysterical I get, my problems are not time sensitive. So, relax, have a cookie, and a very nice day!
Lady Aleena

Replies are listed 'Best First'.
Re: RFC: How did I do writing my first test?
by jcb (Parson) on Sep 24, 2020 at 01:30 UTC

    It looks mostly reasonable to me, although the die on line 11 should probably be BAIL_OUT instead — testing should probably abort immediately if the module to be tested cannot be loaded.

Re: RFC: How did I do writing my first test?
by kcott (Archbishop) on Sep 24, 2020 at 05:24 UTC

    G'day Lady Aleena,

    With regard to this line:

    use v5.10.0;

    Whenever I specify a minimum Perl version, I always put the use VERSION; statement as the first line of code. Let users know immediately if their Perl version is too old. There may be other benefits: such as enabling features; and, if you have use 5.012; (or later) you'll get use strict; for free (see use for details).

    The documentation for use has:

    use 5.024_001; # ditto; older syntax compatible with perl 5.6

    While I generally wouldn't expect people to be using such old versions, i.e. 5.6 or earlier, I typically hedge my bets and use that format just in case.

    In most cases, I don't include a subversion in the use VERSION; statement. However, 5.10.0 had a number of problems which were fixed in 5.10.1 — those changes were incompatible with 5.10.0 (see "perl5101delta: Incompatible Changes"). On the rare occasions that I've needed to specify 5.10 as a minimum version, I've used:

    use 5.010_001;

    — Ken

      Most of the incompatible changes in 5.10.1 were with regards to smart match and switch. use 5.010 is fine if you just want say, state, and //.

        Yes, you are correct. I was aware of this but, not knowing who else was, I provided a link to those changes.

        At the time of writing, the reason for specifying 5.10.0 was unclear. That's since been elucidated (2nd paragraph) and commented upon adequately: I don't have anything further to add to that.

        — Ken

Re: RFC: How did I do writing my first test?
by Arunbear (Prior) on Sep 24, 2020 at 11:14 UTC

    I'm not convinced that testing of a final new line adds any value, especially given that your code looks agnostic to its presence/absence (from both the code writer and code users point of view).

    Putting the tests in subroutines (one for each group of tests) would make the code more skimmable, and save you from needing to use arcane names like ($n_fh, $n_file) i.e. having that in a sub means they would only exist in one place and can have more 'normal' names like in the encoding test case.

    As for the code itself, 'fancy_open' is not an accurate name because it is doing reading and closing as well as opening. In Perl parlance, this is called "slurping", so I'd suggest "fancy_slurp".

    Slurping into an array is fine for small files, but will become inefficient as the input files get larger, so you may want to consider providing an iterator as well.

    Why keep the POD in a separate file? POD is designed to be embeddable in the module and e.g. to veiw it you could just do:

    perldoc My::Module

      Testing files that end with newlines was suggested here. I would remove those tests and happily only have one temp file for all the tests.

      I am having a hard time visualizing the tests in subroutines. I am also willing to expand the variable names. I was being a bit lazy when I wrote them. ($n_fh, $n_file) becomes ($newline_fh, $newline_file); ($no_n_fh, $no_n_file) becomes ($no_newline_fh, $no_newline_file). However, if the tests for files ending with a newline are removed, I will just use the ($fh, $fn) from line 84. I will push the test with the expanded variable names shortly after posting this.

      I will think about changing the name, however, I do not like the word slurp.

      Putting my POD in separate files came about several weeks ago. I was writing up usage of a site specific module for myself, finally, in a separate file. I was going to save it as text. All it needed was a little formatting, so I added the POD formatting to it. I saved it as .pod, committed it, and pushed it to GitHub. Out of curiosity, I looked at it on GitHub and was happy to see the POD was rendered into HTML. Since it is easier to read when rendered as HTML, I decided to do it for all my modules. The only time I ever read a Perl document with perldoc is when I am checking to see what my POD looks like. Otherwise, I read the Perl and module documentation in a browser (perldoc.perl.org or metacpan.org). So, since GitHub will render .pod as HTML, making it easier to read, I thought to myself, "Why not?". Also, perldoc My::Module still works whether the POD is embedded or separate. I checked.

      My OS is Debian 10 (Buster); my perl versions are 5.28.1 local and 5.16.3 or 5.30.0 on web host depending on the shebang.

      No matter how hysterical I get, my problems are not time sensitive. So, relax, have a cookie, and a very nice day!
      Lady Aleena
        Using subroutines could be something as simple as
        sub test_encodings { my ($fh, $fn) = tempfile(); $fh->print($file_string); $fh->close(); is_deeply( [ Fancy::Open::fancy_open($fn) ], [ map encode('utf8', $_), @wanted_array ], "testing an array opened with no encoding" ); my @encodings = qw(UTF-8 ascii cp1252 iso-8859-1); for my $encoding (@encodings) { is_deeply( [ Fancy::Open::fancy_open($fn, { 'encoding' => $encoding }) ], [ map encode($encoding, $_), @wanted_array ], "testing an array opened with $encoding encoding" ); } } sub test_with_file_that_ends_with_a_newline { my ($fh, $fn) = tempfile(); $fh->print("$file_string\n"); $fh->close(); is_deeply( [ Fancy::Open::fancy_open($fn) ], [ @wanted_array ], "testing a plain array with file that ends with a newline" ); is_deeply( [ Fancy::Open::fancy_open($fn, { 'before' => 'solid ' }) ], [ @solid_array ], "testing an array with before option with file that ends with a +newline" ); is_deeply( [ Fancy::Open::fancy_open($fn, { 'after' => ' bead' }) ], [ @bead_array ], "testing an array with after option with file that ends with a n +ewline" ); is_deeply( [ Fancy::Open::fancy_open($fn, { 'before' => 'solid ', 'after' = +> ' bead' }) ], [ @solid_bead_array ], "testing an array with before and after options with file that e +nds with a newline" ); }
        As bonuses, the file name/handle variable names are uniform, and the section comments are made redundant by the sub names.

        On second thought, "slurp" (in Perl land) means reading a file into a scalar, whereas you're reading into an array, so "fancy_read_lines" is still more accurate than the current name.

        I didn't realise GitHub could automatically render .pod files - that is cool.

      Testing the final newline presence/absence is still beneficial to ensure that the code stays agnostic to it. Further, if the code handles those cases differently, testing that processing is needed for code coverage.

        I'm not sure ... (on closer inspection of what it does) that seems like basically retesting Perl's readline function, which is already tested elsewhere.

      After all of the talk about final newlines, and a problem just hit me in the head. I did not take into consideration blank lines in files. What if a file has a blank line or two in the middle? If there is a blank line it currently, the before and after still get put on the list without any other value.

      test.txt (file)
      red orange yellow spring green teal cyan azure blue violet magenta pink white black gray
      Code
      perl -E ' use lib "mods/lib"; use Fancy::Open qw(fancy_open); my @array=fancy_open("test.txt", { before => "solid ", after => " bead +" }); say $_ for @array; '
      Result
      solid red bead solid orange bead solid yellow bead solid spring bead solid green bead solid teal bead solid cyan bead solid azure bead solid bead solid blue bead solid violet bead solid magenta bead solid pink bead solid white bead solid black bead solid gray bead

      So, I am now three steps back from being ready to package this module. *sighs*

      I didn't feel like fighting with splice right now to add an empty line in the test script.

      My OS is Debian 10 (Buster); my perl versions are 5.28.1 local and 5.16.3 or 5.30.0 on web host depending on the shebang.

      No matter how hysterical I get, my problems are not time sensitive. So, relax, have a cookie, and a very nice day!
      Lady Aleena

        This is why you should write full-coverage testsuites — in the course of testing every path, you are likely to find edge cases that you had not recognized before.

        For this particular problem, the easiest solution, assuming that you chomp each input line is to put the code that adds before and after strings in an unless (m/^$/) block. (If you do not chomp the input, change the regex match to m[^$/$], quickly tested here with perl -e 'while (<>) { print "empty\n" if m[^$/$] }' and typing some sample input.)

        What does the documentation say about how it handles blank lines? :)

        i.e. this may well be the correct behaviour (but you'd need to think about and document that).

Re: RFC: How did I do writing my first test?
by perlfan (Vicar) on Sep 24, 2020 at 03:07 UTC
    My first comment is that tests should be as dumb as possible. This also translates to as superficial and verbose as possible. For example, the use of map in the is_deeply tells me that this should probably be unrolled into a series of is. The reason for this is for ease of test maintenace. Tests are also excellent synopses of what they're testing. I often look at tests to see exactly how the module I want to use is intended to function and some clues on how it operates internally. In other words, tests can be used for additional and indepth documentation for the author and users a like.

    Another couple of things: that you don't have to use done_testing and set tests => "14";. If you unroll the is_deeply and base the number of tests on the number of entries in your input files, then go with just the done_testing. This is most useful when the number of tests are based on input length that you may update at some point. Finally, I don't see anything on an initial pass that requires you include use v5.10.0;.

    Overall, I think it's great. And it's even greater that you're actually writing a test! :)

      I changed the is_deeply to is, and all the tests after the one in BEGIN failed. The way I read it is this: is is for comparing strings, is_deeply is for everything else. Since I am comparing two arrays, then I believe I need is_deeply.

      The reason I included use v5.10.0; is because it is the minimum version of Perl needed to use the module since it uses // (defined or). It makes sense to me to use the same version for the test.

      My OS is Debian 10 (Buster); my perl versions are 5.28.1 local and 5.16.3 or 5.30.0 on web host depending on the shebang.

      No matter how hysterical I get, my problems are not time sensitive. So, relax, have a cookie, and a very nice day!
      Lady Aleena
        ... I included use v5.10.0; is because it is the minimum version of Perl needed to use the module since it uses // ...

        To my mind, a use VERSION; statement should generally not be in a unit test script, but in the module that is under test. (Of course, there will be odd cases when such a statement needs to be in a test script, but I expect these cases to be rare.) I try to write unit tests to be compatible with the absolute minimum Perl version, although in practice this works out to be version 5.8.8. :)

        Placing a use VERSION; statement as the first statement in a module (and in scripts as needed) as kcott recommends here is part of compile-time checking of environmental requirements. I always include a version assertion except when the code is so generic as to be digestible by even the most hoary version of Perl. Further, having this statement as the first statement in a module as kcott suggests can immediately alert the reader to a minimum version requirement.

        It is really the business of each module to police its minimum environmental requirements, including Perl version. These requirements may change radically as the module evolves, and you don't want to have to keep track of all that in a test script. Also, a given application may use a wide variety of modules with widely differing minimum requirements. The use_ok test will catch a mismatch between a module and the test environment right from the git-go.


        Give a man a fish:  <%-{-{-{-<

      «I often look at tests to see exactly how the module I want to use is intended to function and some clues on how it operates internally. In other words, tests can be used for additional and indepth documentation for the author and users a like.»

      I read the friendly manuals. And I’m convinced that tests are not made for additional and indepth documentation. Studying the sources is a different thing. Regards, Karl

      «The Crux of the Biscuit is the Apostrophe»

      perl -MCrypt::CBC -E 'say Crypt::CBC->new(-key=>'kgb',-cipher=>"Blowfish")->decrypt_hex($ENV{KARL});'Help

        Tests can be useful as additional documentation. Type::Tiny::Manual::AllTypes explicitly links to a directory in the Type::Tiny test suite that is intended to act as in-depth documentation. It tests combinations of things you might not expect to ever see, so perhaps wouldn't see explicitly documented elsewhere, like "should a blessed arrayref pass the type constraint for negative integers?"

        Certainly most of the test suite is not written with that in mind, but I do think having a test suite that is organized and provides a demonstration of every feature of the software is a worthy goal, and once that goal has been achieved then your tests will act as documentation whether you intended it that way or not.