Beefy Boxes and Bandwidth Generously Provided by pair Networks
P is for Practical

comment on

( #3333=superdoc: print w/replies, xml ) Need Help??
A few months back I bought the book "The Pragmatic Programmer" and read it from front to back and felt it a great addition to my book collection. I have applied many of things they talk about and have found that my performance is improving.

Last year in October I had started down the road of OO everything, some of you can hopefully relate. So all this good OO thinking was going about and I had "improved" my line count, abstraction (to some extent) and code reuse, but I was just not getting where I thought I should be in volume of code produced. In fact I felt it was more difficult to write my (at the time ) throw away "test" scripts.

Last month I started using Test::More with more seriousness. I started using the "Don't test the big end, test the small end" mentality. That is test the smallest amount of code you can and then work your way up. This helped me find ways to split apart different sections of my existing modules into much simpler methods that performed part of, rather then all of the work.

Now I don't have anyone to answer to for the appearance or style of my code, except for myself and I think my code should improve even if the clients will never look at or understand what I have done. So these improvements ( or perceived improvements, time will tell ) are all self driven and constructed from various points of exposure. While I didn't reference any material directly while writing this, I can't say that I didn't paraphrase.

Testing Code

I had thought for sometime that writing test code was a difficult annoying task ( lack of information, I had never really tried to write real tests before ) that was only helpful if you had 10_000+ lines of code to write. How wrong could I be. I took some time and invested in getting past the learning curve on some of the various Test modules that are available and this is what I found:

  • They ARE easy to use once you try them
  • They DO increase your productivity or at least get you to more accurate results quicker then without.
  • They HELP you find bugs that you might otherwise have missed.

I settled on Test::More for most of my testing scripts. With most of the Test modules it is important to remember 1 (one) key thing. True or False

Thats it! Thats all you are testing. Is it or is itn't. Once I saw that is all I was really doing I started liking to write tests. Now there is more you can apply to the mentality and practice of testing, but I think if you are just starting out keep it simple.

Here is simple testing script that runs 6 tests and fails two of them:

use Test::More; # you may need to install it my @test_these = ( 'fruit' , 'animal' , 'tree' , 'goat' ); # The Test::More synopsis suggests you do this # with the use above, but I found that I needed # to set it dynamically after determining how # many tests I was going to run plan tests => scalar(@test_these) + 2; # now we can create a "test" ok( $test_these[0] eq 'fruit' , 'The first element is "fruit"' ); # that's it, that's all you have to do to "test" something. # The ok is the exported function from Test::More and determines # if the first argument is true or false ( 1 or 0 ) # Now I admit that was a stupid test, since we have a # hard coded array so lets do something else foreach (@test_these) { ok( m/i/ , "our element has an 'i' in it" ); } # That will test to see if the list item has an 'i' # in it and in this case some of the tests will fail. # one last test undef @test_these ok( !@test_these , 'no more @test_these!' ); 1;

So how that evolve into better code? Lets take a more complicated process and see how we can improve it.

Our initial method "requires" that it returns the content of a file that is passed to it. We don't do any file type checks "assuming" |-) this is a semi private method.:

sub get_file_contents { my ($self,$args) = @_; my $contents; my $file = $args->{file_name}; if (!$file) { ($file) = $self->fancy_magic_file_find($args); } if (!$file || !-e $file) { return "ERROR: Invalid file passsed to 'get_file_contents'"; } open(FILE,"$file") or $self->error_to_log("ERROR"); $contents = do {local $/;<FILE>}; close FILE; $self->error_to_log("retrieved $file") if $contents; return $contents; }

Now to test that code we do something like this:
use Test::More tests => 3; use My::Module; my $object = My::Module->new(); # test that the object exists ok( $object->isa('My::Module' , "Our object is in the right class"); my $content = $object->get_file_contents( { file_name => '/home/trs80/file.txt' } ); ok( $content ne '' , "\$content has something in it!" ); ok( $content =~ /string/ , "\$content contains string!" ); 1;

Well that worked out OK , but I noticed somethings during my testing. There was no easy way for me to get back the file name, since I might want to do something to the contents and write it back out to the original file. It also assumed I would be passing in a hashref which is over kill if I know the name of file already, which I most likely will at some point in the testing. This is how I come to the coding to test part. I want to modify this method so my testing is easier and at the same time it has the side effect of making the code useful in more places, which increases the likelihood of me reusing it for other things.

So I do two things, make it so the method will return a list of items, the content and the file name or just the content based on the request type and it will allow for a normal string to be passed in which we will assume is the file name.

sub get_file_contents { my ($self,$args) = @_; my $contents; my $file; if (ref($args) eq 'HASH') { $file = $args->{file_path}; } else { $file = $args; } if (!$file) { ($file) = $self->fancy_magic_file_find($args); } if (!$file || !-e $file) { return "ERROR: Invalid file passsed to 'get_file_contents'"; } open(FILE,"$file") or $self->error_to_log("ERROR: "); $contents = do {local $/;<FILE>}; close FILE; $self->error_to_log("retrieved $file") if $contents; return wantarray ? ($contents,$file) : $contents; }

Lets refactor our test code slightly to test the new features.

use Test::More tests => 13; use My::Module; # Make my object my $object = My::Module->new(); my $dir = '/home/trs80'; my $test_file = 'file.txt'; # create hashref for use later on my $session = { base_dir => $dir, } # test that the object exists ok( $object->isa('My::Module' , "Our object is in the right class"); # first we try a file name by itself my $content = $object->get_file_contents( "$dir/$test_file" ); ok( $content ne '' , "\$content has something in it!" ); ok( $content =~ /string/ , "\$content contains string!" ); undef $content; ok( !$content , "\$content is empty" # now we try a file name like the first test. $content = $object->get_file_contents( { file_name => "$dir/$test_file", } ); ok( $content ne '' , "\$content has something in it!" ); ok( $content =~ /string/ , "\$content contains string!" ); undef $content; ok( !$content , "\$content is empty" ); # test of the 'fancy_magic_file_find' way # and make sure it gives back a file name my ($contents,$file_name) = $object->get_file_contents( $session ); ok( $file_name =~ m#^$dir#, "our file_name: $file_name" ); ok( $contents ne '' , "\$content has something in it!" ); ok( $contents =~ /string/ , "\$content contains string!" ); undef $contents; ok( !$contents , "\$content is empty" ); # test the error condition $content = $object->get_file_contents(); ok( $content =~ /^ERROR\:/ , 'error on retrieve with no arguments' ); 1;

After looking at how much testing code there is you might be thinking that you would be spending all your time writing test code. While the volume of test code SHOULD be greater then the "worker" code it is by no means as difficult to write. Since all you are doing is testing each possible condition under which your worker code will perform you are just testing for true and false on several "real problems" and only making potentially small problems. I say problems because testing scripts are prone to bugs as well, but since not nearly as much complex thought is going on in them you should be able to quickly determine if it is your test or your worker code that is wrong.

The other benefit of writing tests is the ability to easily check for broken code along the way, aka regression testing, or in other words does it still do x when you add y. Your client/boss calls you and says that they need feature xyz installed. Now you can add another test for its specific functions and rerun your test script to at least be some what confident that you haven't made a mess of things.

UPDATE: missed some semi colons in my code blocks, maybe I should have tested it, how ironic :) and added a readmore tag.

In reply to Re: Further adventures with testing by trs80
in thread Further adventures with testing by Ovid

Use:  <p> text here (a paragraph) </p>
and:  <code> code here </code>
to format your post; it's "PerlMonks-approved HTML":

  • Are you posting in the right place? Check out Where do I post X? to know for sure.
  • Posts may use any of the Perl Monks Approved HTML tags. Currently these include the following:
    <code> <a> <b> <big> <blockquote> <br /> <dd> <dl> <dt> <em> <font> <h1> <h2> <h3> <h4> <h5> <h6> <hr /> <i> <li> <nbsp> <ol> <p> <small> <strike> <strong> <sub> <sup> <table> <td> <th> <tr> <tt> <u> <ul>
  • Snippets of code should be wrapped in <code> tags not <pre> tags. In fact, <pre> tags should generally be avoided. If they must be used, extreme care should be taken to ensure that their contents do not have long lines (<70 chars), in order to prevent horizontal scrolling (and possible janitor intervention).
  • Want more info? How to link or or How to display code and escape characters are good places to start.
Log In?

What's my password?
Create A New User
Domain Nodelet?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others perusing the Monastery: (3)
As of 2022-09-29 17:08 GMT
Find Nodes?
    Voting Booth?
    I prefer my indexes to start at:

    Results (125 votes). Check out past polls.