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

I am trying to write tests for a class that reads input from a file and creates objects of another class from that data. I've already written tests for the latter class, but I'm stuck on how to write tests for the former.

For example, the Widget class looks something like this:

package Widget; sub new { my ( $class, %params ) = @_; # etc } sub get_color { ... }
This class is fairly straightforward and I wrote a series of tests for it. No problem.

I also have a Container class that holds Widget objects. It looks something like this:

package Container; sub new { ... } sub load_widgets { my ( $self, $filename ) = @_; open( my $infh, '<', $filename ) or croak "..."; while( my $line = <$infh> ) { # parse data my $widget_object = Widget->new( %params ); $self->{_Container}{$id} = $widget_object; } } sub get_widgets_by_color { my ( $self, $color ) = @_; # etc }

I need to write tests for the Container class, but to do that I need to create a Container object with some test data. What is the best way to achieve this? I thought of a number of ways to approach this problem:

  1. Provide an input file that contains test data along with the container.t file.
    This means that the tests are dependent on both the successful loading and operation of Widget.pm and the input file that contains the test data. It also means that the test data is separate from the container.t file, so it would be easy for the two to get out of sync.
  2. Have the test file create an input file with test data at runtime, then delete it when the tests are complete.
    This approach would keep the test data in the same file as the tests themselves, but it would be dependent on creating the file successfully. It would also be dependent on the successful loading and operation of Widget.pm and the input file that contains the test data (as above).
  3. Use Test::MockObject and/or Test::MockModule to mock the Widget class.
    This would eliminate the dependency on Widget.pm but it would require providing mock methods where needed and it wouldn't allow me to do any integration testing. It also wouldn't eliminate the dependency on the input file containing the test data.
  4. Refactor Container.pm to separate the open statement from the code that processes the data.
    This would allow me to pass a filehandle (such as \*DATA) to the routine, which would eliminate the dependency of an external test input file (the method containing the open code simply wouldn't get tested), but it seems like an unnecessary complication to Container.pm added simply for the sake of testing. Also, it would still be dependent on the successful loading and operation of Widget.pm.
  5. Refactor Container.pm to separate the widget storage statement from the code that processes the data.
    By creating an add_widget method, I could use load_widgets to only read the input data. I could skip testing load_widgets and simply use add_widget to create the Container object for the rest of the tests. This approach would also be dependent on the successful loading and operation of Widget.pm.

The idea of creating an add_widget method appeals to me. That, combined with mocking the Widget class, is the direction that I'm leaning towards. I'm still learning how to design classes and write tests, though, so please whack me with the clue stick.

Which one (or more) of these approaches sounds like the best design? Are there other alternatives that aren't listed here? I've never used Test::MockObject or Test::MockModule, so if that's the best way to go I'd appreciate some pointers.

Many thanks in advance.

Replies are listed 'Best First'.
Re: Testing methods that read input from files
by davidrw (Prior) on Jul 22, 2006 at 21:08 UTC
    nothing wrong w/#1 as along as the files can be found (since they'll be under the distro path)

    #2 should be easy and complete ; File::Temp and File::Copy are both useful here .. the first to get a file name (and to automatically clean it up) and the second to copy \*DATA if that's what's desired (or it's simple enough to do manually as well).

    This also has the advantage that you can create the content on the fly, in case it needs to be based upon previous tests or results (e.g. a general example: insert'ing into a db, and the datafile needs to have that new primary key value or something)
Re: Testing methods that read input from files
by Herkum (Parson) on Jul 23, 2006 at 13:36 UTC

    For #1,

    It also means that the test data is separate from the container.t file, so it would be easy for the two to get out of sync.

    Your data in a test file should not become out of sync if you are properly managing your tests. The tests are there to point out your short-comings and that would include problems with your test data.

    For #2, if you cannot keep your data straight in a seperate test file why should generating an input file should be superior? You can make mistakes with the test file that you generate and add another area you have to debug.

    For #3, I would only use the Mock tests when you have to replicate an environment that needs a special setup and will not run any other way. An example, a module that you have to run with mod_perl on Apache. When you are running your tests during the build process it obivious you don't have mod_perl so the Test::Mock* would be good here.

    for #4, I would not extend code in my modules to support testing. Debugging, yes, testing, no. It means that you have not thought enough about how to setup your tests.

    For #5, like #4 you are changing your code to support testing and would advise against it.


    If you are going to run a comprehensive test suite with randomly generated data, I would suggest generating a test file, otherwise stick with a static pregenerated test file.

Re: Testing methods that read input from files
by Anonymous Monk on Jul 25, 2006 at 08:37 UTC
    You could give the filename "<&DATA" to your module, if you wouldn't insist on the 3-arg form of open. That is a design defect in your class. Another way would be
    sub load_widgets { my($self,$infh)=@_; # ^ IMPORTANT!!! $infh=new IO::File("<",$infh)unless ref$infh; #sorry open($fh...) needs undef ... }
    if you think of preventing magic open by the user.