http://qs1969.pair.com?node_id=11121949

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

I am going to start writing tests for my Fancy::Open module that needs a file to open. Test::MockObject was suggested. In reading the beginning of the documentation, I saw it can mock arrays, scalars, code, and globs, but I do not see files.

my $mock_array = Test::MockObject->new( [] ); my $mock_scalar = Test::MockObject->new( \( my $scalar ) ); my $mock_code = Test::MockObject->new( sub {} ); my $mock_glob = Test::MockObject->new( \*GLOB );

So, can Test::MockObject mock a file? If yes, how does it work? I am having a difficult time understanding the documentation.

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: Can Test::MockObject mock a file?
by tobyink (Canon) on Sep 19, 2020 at 21:04 UTC

      fancy_open opens a file, runs the contents through a while that chomps it, and prepends the lines with the string in the before option and/or appends the string in the after option. It returns an array.

      So what I need is a file to open and manipulate. The contents of the file do not need to be spectacular, and I will probably use my favorite dummy list.

      red orange yellow spring green teal cyan azure blue violet magenta pink white black gray

      I am willing to use an actual file for the test, but I am unsure where to put it. Do I put it in the t directory with the test? Is it better to use a mock file instead? I would like to get this module's tests written first. I am finally learning how to write tests, so please bear with me for a short while?

      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 would suggest putting that file in your t/ directory, using FindBin to get the full path to your t/ directory, and then passing that path to your module.

        Your test script can use File::Temp to create a file for you that you write to, and that you then can read back in your test. Done correctly, the file will be removed on end of scope. I usually prefer creating a tempdir with a template such as testname-XXXXX. That way I can create any files I want within that directory, and it will get torn down on close of scope; I don't have to bother with temp files for each thing that needs a file, so long as I put everything ephemeral into the tempdir.

        A staunch unit-test advocate would say that unit tests shouldn't touch the filesystem unless they're specifically testing low level filesystem operations. It may be possible for your tests to open filehandles to in-memory files instead of touching the filesystem. The documentation for open discusses how to do this, I think. If it doesn't, then perlopentut would be the other place to look. But in short:

        my $in_string = "Hello world.\n"; my $out_string = ''; open my $infh, '<', \$in_string; open my $outfh, '>', \$out_string; while(<$infh>) { print $outfh "$.: $_"; } print $out_string; # prints 1: Hello world.\n

        So here I gave Perl a variable containing "Hello world.\n" and told it to treat it as a file, allowing $infh to stream through the pseudo-file. I also gave Perl an empty string for output, and printed the input file to the output file, with a line number prepended. Finally, I printed the contents of the output string, and it contained the contents of the input string, but with a line number prepended.

        This technique is great for tests, because it allows you to test in isolation of the filesystem. Later, you can independently test that when you create files (in a tempdir) they have the appropriate names, permissions, and owner.

        My preference is to test as much as possible in isolation of the filesystem, but then in more functional tests, also verify things like the characteristics of created files, and the ability to read the files we need to be able to read. That way I can do really fast testing of things like what contents are dumped to or read from files, but can also verify that I'm working effectively within the environment.


        Dave

      I went back and gave File::Temp a try. I did not like the idea of putting files for these tests on a user's filesystem. Here is what I wrote, with the suggestions to make my test code less repetitive and to test whether a newline or lack thereof at the end of the file would matter. I hope cheating and using the @wanted array to make the temp test files is not a bad thing.

      Note: I cargo culted the use of File::Temp from the one place in my code where I used it before. I am still a little fuzzy on how it works, but it does appear to work and the tests still passed.

      #!perl use strict; use warnings; use v5.10.0; use Test::More tests => "9"; use File::Temp qw(tempfile); 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 = qw(red orange yellow spring green teal cyan azure blue vi +olet magenta pink white black gray); # Creating file that ends with a newline my ($n_fh, $n_file) = tempfile(); $n_fh->print(join("\n", @wanted)."\n"); $n_fh->close(); # Testing with file that ends with a newline is_deeply( [ Fancy::Open::fancy_open($n_file) ], [ @wanted ], "testing a plain array with file that ends with a newline" ); is_deeply( [ Fancy::Open::fancy_open($n_file, { 'before' => 'solid ' }) ], [ map "solid $_", @wanted ], "testing an array with before option with file that ends with a newl +ine" ); is_deeply( [ Fancy::Open::fancy_open($n_file, { 'after' => ' bead' }) ], [ map "$_ bead", @wanted ], "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' }) ], [ map "solid $_ bead", @wanted ], "testing an array with before and after options with file that ends +with a newline" ); # Creating file that does not end with a newline my ($no_n_fh, $no_n_file) = tempfile(); $no_n_fh->print(join("\n", @wanted)); $no_n_fh->close(); # Testing with file that does not end with a newline is_deeply( [ Fancy::Open::fancy_open($no_n_file) ], [ @wanted ], "testing a plain array with file that does not end with a newline" ); is_deeply( [ Fancy::Open::fancy_open($no_n_file, { 'before' => 'solid ' }) ], [ map "solid $_", @wanted ], "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' }) ], [ map "$_ bead", @wanted ], "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' }) ], [ map "solid $_ bead", @wanted ], "testing an array with before and after options with file that does +not end with a newline" ); done_testing();

      I have not figured out how to test the encoding option for this module.

      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 did not like the idea of putting files for these tests on a user's filesystem.

        Think of it this way; it's difficult to anticipate all of the ways user filesystems might differ from your expectations. One might be encrypted. Another might be case-insensitive. A third might be on a slow network mount.

        By testing as much of that system as possible—by not mocking things—your tests are richer and better represent the conditions you want to validate.

        Using File::Temp avoids at least two difficult problems. First, it reduces your need to clean up after yourself by handling it for you. Hopefully that eases your mind about writing to user filesystems. Second, it makes your tests less serial; you can run them in parallel because there's very little chance multiple tests or multiple runs will collide because they all depend on the state of one specific file with a hard-coded name and path.

        If you wrap your temporary file generation in a function, it could be even cleaner. The third time you write to a temporary file, I'd do that; you'll reduce duplication and further reduce coupling on the generated name of the temporary file.

Re: Can Test::MockObject mock a file?
by perlfan (Vicar) on Sep 19, 2020 at 23:37 UTC
    Test::MockFile::FileHandle exists. Also, file handles can be "mocked" using tie and implementing a null file handle interface, if one doesn't exist. See perltie on this. File::Temp is also used extensively in testing scenarios, as mentioned already.