The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Test::Class::Tutorial - A tutorial on testing with Test::Class.

DESCRIPTION

Note: This is a first draft. There maybe mistakes. All feedback welcome.

This tutorial provides a brief introduction to the major features of Test::Class. If you are not already familiar with Test::Simple, Test::More and friends then you should work your way through Test::Tutorial first.

Test::Class can provide some advantages over traditional *.t test scripts - see "INTRODUCTION" in Test::Class for more details.

We'll work through the creation of a few simple classes, along with a simple test script, and show how you can easily refactor your test script into a test class.

THE CLIENT'S BRIEF

"I need to keep track of credits and debits on an account. I need to know the current balance and keep track of the order the transactions occurred in."

THE ACCOUNT OBJECT

After a brief discussion with the client we decide to create a simple Account object to keep track of the credit and debit transactions. I start by using h2xs to create a template for the module (see perlnewmod if you've not used h2xs before).

  % h2xs -AX -n Account
  % cd Account
  % ls
  Account.pm  Changes  MANIFEST  Makefile.PL  README  test.pl

CREATING AN ACCOUNT

The first thing I need to be able to do is create new Account objects. Since I tend to write my code "Test First" the first thing I do is write a test to check that I can create Account objects in the new test.pl. I can do this simply using Test::More:

  #! /usr/bin/perl -w

  use Test::More tests => 1;
  use strict;
  use Account;

  my $account = Account->new;
  isa_ok($account, 'Account');

Now if I run:

  % perl Makefile.PL
  % make test

I get

  Can't locate object method "new" via package "Account" (perhaps you   
  forgot to load "Account"?) at test.pl line 7.

Not much of a surprise since I haven't written the new method yet. Time to add new() to Account.pm

  package Account;

  use strict;
  use warnings;

  sub new {
      my $class = shift;
      my $self = {};
      return bless $self, $class;
  };

  1;

That should do it. I check by running make test again.

  1..1
  ok 1 - The object isa Account

Success!

INITIAL BALANCE

Next I want to find out the current balance of an account. This should initially be zero - so I add another test to test.pl.

  is($account->balance, 0, 'initial balance zero');

Unsurprisingly I get an error when I do make test since I've not written the balance method yet. I do the "Simplest Thing That Can Possibly Work" and add the following to Account.pm.

  sub balance { 0 };

Let's do another make test to make sure everything works.

  1..1
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  # Looks like you planned 1 tests but ran 1 extra.
  make: *** [test_dynamic] Error 1

Darn. I forgot to update the number of tests in test.pl. I need to do:

  use Test::More tests => 2;

Try again.

  1..2
  ok 1 - The object isa Account
  ok 2 - initial balance zero

Yay!

CREDITING ACCOUNTS

I need to credit the account, so a credit method seems sensible.

I generally throw exceptions (i.e. die) to indicate errors - so I need to check that credit exits normally. I can do this using "lives_ok" in Test::Exception, so I add:

  use Test::Exception;

and:

  lives_ok {$account->credit(10)} 'credit 10 worked';

to test.pl not forgetting to update the number of tests:

  use Test::More tests => 3;

Just to check that the test works I do make test

  ok 1 - The object isa Account
  ok 2 - initial balance zero
  not ok 3 - credit 10 worked

Not very surprising since there isn't a method yet. So I add one to Account.pm

  sub credit {};

Now I get

  1..3
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked

Excellent.

Now I need to check that the credit has updated the balance of the account appropriately. Time for another test:

  is($account->balance, 10, 'new balance 10');

and, of course, remember to update the number of tests:

  use Test::More tests => 4;

Now I get.

  1..4
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  not ok 4 - new balance 10
  #     Failed test (test.pl at line 13)
  #          got: '0'
  #     expected: '10'
  # Looks like you failed 1 tests of 4.

Since both balance() and credit() are stubs this result shouldn't be entirely unexpected.

Time to think about the implementation.

Since I need to track credits and debits an array of numbers seems to the the "Simplest Thing That Can Possibly Work". Positive for credits, negative for debits. I start re-writing Account.pm with that in mind.

New accounts have no transactions, so I need to start with an empty array. So new becomes.

  sub new {
      my $class = shift;
      my $self = {
          transactions => [],
      };
      return bless $self, $class;
  };

Just to make sure that I've not broken anything I run make test.

  1..4
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  not ok 4 - new balance 10
  #     Failed test (test.pl at line 13)
  #          got: '0'
  #     expected: '10'
  # Looks like you failed 1 tests of 4.

No new bugs are introduced so I move onto the balance method.

  sub balance {
      my $self = shift;
      my $transactions = $self->{transactions};
      my $total = 0;
      $total += $_ foreach @$transactions;
      return($total);
  };

Again, I run make test just to double check that nothing has changed. Finally, I get to the credit method and get:

  sub credit {
      my ($self, $amount) = @_;
      my $transactions = $self->{transactions};
      push @$transactions, $amount;
  };

Now running make test gives me:

  1..4
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  ok 4 - new balance 10

Excellent!

I check that this isn't a fluke by adding another test.

  lives_ok {$account->credit(32)} 'credit 32 worked';
  is($account->balance, 42, 'new balance 42');

Remembering, of course, to update the number of tests:

  use Test::More tests => 6;

now I get

  1..6
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  ok 4 - new balance 10
  ok 5 - credit 32 worked
  ok 6 - new balance 42

All working. Good.

TRACKING TRANSACTIONS

I need to "keep track of the order the transactions occurred in" so I'll provide a method to return a list of the transactions. Of course, I write a test first.

  my @transactions = $account->transactions;
  is_deeply(\@transactions, [10, 32], 'transactions ok');

and update the number of tests

  use Test::More tests => 7;

Running make test tells me I need to implement the method.

  sub transactions {
      my $self = shift;
      return(@{$self->{transactions}});
  };

Run make test and I get

  1..7
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  ok 4 - new balance 10
  ok 5 - credit 32 worked
  ok 6 - new balance 42
  ok 7 - transactions ok

Everything works. Good.

DEBITING ACCOUNTS

Okay. Time to implement debits. First I need some tests.

Since I'm feeling lazy I'll take the credit tests and change them to call debit and check for negative balances.

  lives_ok {$account->debit(10)} 'debit 10 worked';
  is($account->balance, -10, 'new balance -10');
  lives_ok {$account->debit(32)} 'debit 32 worked';
  is($account->balance, -42, 'new balance -42');

and update the number of tests

  use Test::More tests => 11;

Running make test shows that the tests fail because I haven't written a debit method yet.

  sub debit {
          my ($self, $amount) = @_;
          my $transactions = $self->{transactions};
          push @$transactions, -$amount;
  };

Now what does make test give us:

  1..11
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  ok 4 - new balance 10
  ok 5 - credit 32 worked
  ok 6 - new balance 42
  ok 7 - transactions ok
  ok 8 - debit 10 worked
  not ok 9 - new balance -10
  #     Failed test (test.pl at line 22)
  #          got: '32'
  #     expected: '-10'
  ok 10 - debit 32 worked
  not ok 11 - new balance -42
  #     Failed test (test.pl at line 24)
  #          got: '0'
  #     expected: '-42'
  # Looks like you failed 2 tests of 11.

Oops. Not what I expected. The balance() isn't what I expected.

A quick sanity check shows that I forgot that the credit() test have left the balance() at 42, when I assumed it started at 0 in by debit() tests.

This kind of thing can be a problem when test scripts become larger. Different parts of the test script start intefering with each other making changes to the script harder. Three possible solutions come to mind:

  • I could move the debit tests into a separate test script so that the credit tests cannot affect it. This is too much work for me since I'm feeling lazy.

  • I could alter the expected values in the debit() tests to take the credit() tests into account. This is bad, since I would have to update these values every time I change the credit() tests.

  • I could run the debit() tests on a new account object, so the balance() will start at the value I expected.

I decide to create a new Account object so the debit() tests now look like:

  $account = Account->new;
  ok($account->debit(10), 'debit 10 worked');
  is($account->balance, -10, 'new balance -10');
  ok($account->debit(32), 'debit 32 worked');
  is($account->balance, -42, 'new balance -42');

Run make test again and I get:

  1..11
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  ok 4 - new balance 10
  ok 5 - credit 32 worked
  ok 6 - new balance 42
  ok 7 - transactions ok
  ok 8 - debit 10 worked
  ok 9 - new balance -10
  ok 10 - debit 32 worked
  ok 11 - new balance -42

Excellent - everything works!

REFACTORING ACCOUNT

Even though everything works I should spend a little time in "Mercyless Refactoring" to remove redundancy from the code and make it's future maintaince easier.

If I look at the credit() & debit() methods I see that they are doing almost exactly the same thing. Separate out the common elements and I get.

  sub _add_transaction {
          my ($self, $amount) = @_;
          my $transactions = $self->{transactions};
          push @$transactions, $amount;
  };

  sub credit {
          my ($self, $amount) = @_;
          $self->_add_transaction($amount);
  };

  sub debit {
          my ($self, $amount) = @_;
          $self->_add_transaction(-$amount);
  };

Running make test reassures us that the change hasn't broken anything. Everything else looks reasonable so I'll leave Account.pm for now.

However, I've still not quite finished.

REFACTORING TESTS

Now let's examine test.pl. I have five separate test cases in test.pl, using a total of 11 tests.

  1. Checking that an Account object is created (1 test)

  2. Checking that the initial balance is zero (1 test)

  3. Checking that you can credit the account (4 tests)

  4. Checking that you can see transactions (1 test)

  5. Checking that you can debit the account (4 tests)

I can use Test::Class to make the grouping of the tests much more explicit, and therefore make the test code much easier to undertand and maintain.

With the exception of the transaction test each test case starts with a new Account object. In the testing world such a common object (or set of objects) is called a fixture. I can use Test::Class to create common test fixtures and make the test cases independent of each other, again making code maintainence easier.

While the Account tests are simple enough that I don't really have problems understanding them I'll refactor it into a Test::Class, anyway since this is supposed to be a tutorial on Test::Class!

CREATING A TEST CLASS

The first thing I need to do is declare thr new test class. A test class is just something that inherits from Test::Class. I use the convention of adding the suffix ::Test to the name of the class I am testing. So I add:

  package Account::Test;
  use base qw(Test::Class);

to the top of test.pl. I quickly run make test to check that I've not broken anything.

REFACTORING TESTS INTO TEST METHODS

Now I can start turning our tests into test methods. A test method is a method that has the Test attribute. I replace the test for an Account object being created:

  isa_ok($account, 'Account');

with the test method:

  sub account_creation : Test {
          isa_ok($account, 'Account');
  };

  Account::Test->runtests;

The runtests() method will actually call the method and run the test.

I now run make test

  1..11
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  ok 4 - new balance 10
  ok 5 - credit 32 worked
  ok 6 - new balance 42
  ok 7 - transactions ok
  ok 8 - debit 10 worked
  ok 9 - new balance -10
  ok 10 - debit 32 worked
  ok 11 - new balance -42

and everything still works. Good.

In a similar way I can replace:

  is($account->balance, 0, 'initial balance zero');

with:

  sub balance : Test {
          is($account->balance, 0, 'initial balance zero');
  };

I do not have to add another call to runtests(), it runs all of the test methods for no wherever they are declared. I run make test to double check and everything still passes.

TEST METHODS WITH MORE THAN ONE TEST

There are four separate test for the credit() method:

  lives_ok {$account->credit(10)} 'credit 10 worked';
  is($account->balance, 10, 'new balance 10');
  lives_ok {$account->credit(32)} 'credit 32 worked';
  is($account->balance, 42, 'new balance 42');

You can have test methods that run more than one test by including the number of tests in brackets:

  sub credit : Test(4) {
          lives_ok {$account->credit(10)} 'credit 10 worked';
          is($account->balance, 10, 'new balance 10');
          lives_ok {$account->credit(32)} 'credit 32 worked';
          is($account->balance, 42, 'new balance 42');
  };

Running make test confirms that I've not broken anything. I can now convert the other test cases to methods.

The transaction test:

  my @transactions = $account->transactions;
  is_deeply(\@transactions, [10, 32], 'transactions ok');

can be written as:

  sub credit_transactions : Test {
          my @transactions = $account->transactions;
          is_deeply(\@transactions, [10, 32], 'transactions ok');
  };

The debit tests:

  $account = Account->new;
  lives_ok {$account->debit(10)} 'debit 10 worked';
  is($account->balance, -10, 'new balance -10');
  lives_ok {$account->debit(32)} 'debit 32 worked';
  is($account->balance, -42, 'new balance -42');

Can be written as:

  sub debit : Test(4) {
          $account = Account->new;
          lives_ok {$account->debit(10)} 'debit 10 worked';
          is($account->balance, -10, 'new balance -10');
          lives_ok {$account->debit(32)} 'debit 32 worked';
          is($account->balance, -42, 'new balance -42');
  };

I run make test one more time and confirm that nothing has broken.

AUTOMATIC PLANS

Now that all of our tests are in test methods we can take advantage of one of the nice features of Test::Class - runtests() can calculate the number of tests all the methods run and output an appropriate test header.

This means we can replace:

  use Test::More tests => 11;

with

  use Test::More;

and make test will still produce

  1..11
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  ok 4 - new balance 10
  ok 5 - credit 32 worked
  ok 6 - new balance 42
  ok 7 - transactions ok
  ok 8 - debit 10 worked
  ok 9 - new balance -10
  ok 10 - debit 32 worked
  ok 11 - new balance -42

You can add tests to any of the methods and Test::Class will automatically update the header. Since the number of tests is next to the method you are updating I find it easier to keep the numbers up to date.

For example, the current credit_transactions() method doesn't check any debits. We can fix this by adding another test:

For example, we can add some more tests to the debit method.

  sub credit_transactions : Test(2) {
          lives_ok {$account->debit(10)} 'debit 10 worked';
          my @transactions = $account->transactions;
          is_deeply(\@transactions, [10, 32, -10], 'transactions ok');
  };

make test will now give us:

  1..12
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  ok 4 - new balance 10
  ok 5 - credit 32 worked
  ok 6 - new balance 42
  ok 7 - debit 10 worked
  ok 8 - transactions ok
  ok 9 - debit 10 worked
  ok 10 - new balance -10
  ok 11 - debit 32 worked
  ok 12 - new balance -42

Note how the test header has been updated to tell Test::Harness to expect 12 tests.

REMOVING DEPENDENCIES BETWEEN TEST METHODS

At the moment all of the test methods use the file scoped $account lexical to store the object they are testing. This is bad for two reasons:

  1. It makes subclassing harder since subclasses cannot see the lexical

  2. It introduces dependences between the tests.

Consider the order the test methods are run in.

  • The balance() test would fail if it was run after credit() or debit() - since it relies on no transactions having been made.

  • credit_transactions() would fail if it was run before credit() or after debit() - since it relies on the transactions made by the credit() tests.

This makes maintaining tests a pain since altering one test method can cause other tests to fail.

(The curious amongst you may be asking "what order are test methods run in?" Alphabetical is the answer.)

I can easily fix this by having each test method create its own Account object like this:

  sub account_creation : Test {
          my $account = Account->new;
          isa_ok($account, 'Account');
  };

  sub balance : Test {
          my $account = Account->new;
          is($account->balance, 0, 'initial balance zero');
  };

  sub credit : Test(4) {
          my $account = Account->new;
          lives_ok {$account->credit(10)} 'credit 10 worked';
          is($account->balance, 10, 'new balance 10');
          lives_ok {$account->credit(32)} 'credit 32 worked';
          is($account->balance, 42, 'new balance 42');
  };

  sub credit_transactions : Test(4) {
          my $account = Account->new;
          lives_ok {$account->credit(10)} 'credit 10 worked';
          lives_ok {$account->credit(32)} 'credit 32 worked';
          lives_ok {$account->debit(10)} 'debit 10 worked';
          my @transactions = $account->transactions;
          is_deeply(\@transactions, [10, 32, -10], 'transactions ok');
  };

  sub debit : Test(4) {
          my $account = Account->new;
          lives_ok {$account->debit(10)} 'debit 10 worked';
          is($account->balance, -10, 'new balance -10');
          lives_ok {$account->debit(32)} 'debit 32 worked';
          is($account->balance, -42, 'new balance -42');
  };

Notice how we had to add a couple of calls to credit() in credit_transactions to make the test pass. The advantage is, of course, that it's now independent of the other test methods.

If we run make test we now get:

  1..14
  ok 1 - The object isa Account
  ok 2 - initial balance zero
  ok 3 - credit 10 worked
  ok 4 - new balance 10
  ok 5 - credit 32 worked
  ok 6 - new balance 42
  ok 7 - credit 10 worked
  ok 8 - credit 32 worked
  ok 9 - debit 10 worked
  ok 10 - transactions ok
  ok 11 - debit 10 worked
  ok 12 - new balance -10
  ok 13 - debit 32 worked
  ok 14 - new balance -42

It all works. Good.

SETUP METHODS

I now notice that every test method calls Account->new. If I ever want to change the way Account objects are created I would have to change every method. Since I like to write code "Once and only once" I want to remove this duplication.

Test::Class allows you to define setup and teardown methods. All of a classes setup methods are run before every test method. All of a classes teardown methods are run after every test method has completed.

So we can create a new Account object before every test method by creating a setup method that stores the new Account in the test object that is passed to every test method.

  sub account_fixture : Test(setup) {
          my $self = shift;
          $self->{account} = Account->new;
  };

and we can access it like this.

  sub account_creation : Test {
          my $account = shift->{account};
          isa_ok($account, 'Account');
  };

Running make test confirms that every still works as expected.

Doing this for all the other methods gives us...

  package Account::Test;
  use base qw(Test::Class);
  use strict;
  use Test::More;
  use Test::Exception;
  use Account;

  sub account_fixture : Test(setup) {
          my $self = shift;
          $self->{account} = Account->new;
  };

  sub account_creation : Test {
          my $account = shift->{account};
          isa_ok($account, 'Account');
  };

  sub balance : Test {
          my $account = shift->{account};
          is($account->balance, 0, 'initial balance zero');
  };

  sub credit : Test(4) {
          my $account = shift->{account};
          lives_ok {$account->credit(10)} 'credit 10 worked';
          is($account->balance, 10, 'new balance 10');
          lives_ok {$account->credit(32)} 'credit 32 worked';
          is($account->balance, 42, 'new balance 42');
  };

  sub credit_transactions : Test(4) {
          my $account = shift->{account};
          lives_ok {$account->credit(10)} 'credit 10 worked';
          lives_ok {$account->credit(32)} 'credit 32 worked';
          lives_ok {$account->debit(10)} 'debit 10 worked';
          my @transactions = $account->transactions;
          is_deeply(\@transactions, [10, 32, -10], 'transactions ok');
  };

  sub debit : Test(4) {
          my $account = shift->{account};
          lives_ok {$account->debit(10)} 'debit 10 worked';
          is($account->balance, -10, 'new balance -10');
          lives_ok {$account->debit(32)} 'debit 32 worked';
          is($account->balance, -42, 'new balance -42');
  };

  Account::Test->runtests;

I can now do what I like with the account object in each test method without worrying about it affecting the other methods, and I only have one piece of code to update if I ever change the way Account objects are created.

Running make test again should confirm everything is still working. If you do

 make test TEST_VERBOSE=1 

runtests() will display the name of each test method before it runs like this:

# Account::Test->account_creation 1..13 ok 1 - The object isa Account # Account::Test->balance ok 2 - initial balance zero # Account::Test->credit ok 3 - credit 10 worked ok 4 - new balance 10 ok 5 - credit 32 worked ok 6 - new balance 42 # Account::Test->credit_transactions ok 7 - transactions ok # Account::Test->debit ok 8 - debit 10 worked ok 9 - new balance -10 ok 10 - debit 32 worked ok 11 - new balance -42 ok 12 - debit 8 worked ok 13 - new balance -50

which can help seeing what method a test failed in.

EXTREME JARGON

Test First

Write the test before you write the code.

Simplest Thing That Can Possibly Work

Don't overcomplicate.

Mercyless Refactoring

Improve the code.

Once and only once

don't duplicatte code

BUGS

If you have problems with this documentation please let me know by e-mail, or report the problem with http://rt.cpan.org/.

TO DO

Rationalise this tutorial. It's way too long and idiosincratic.

AUTHOR

Adrian Howard <adrianh@quietstars.com>

If you can spare the time, please drop me a line if you find this documentation useful.

SEE ALSO

Test::Class

Easily create test classes in an xUnit style.

Test::Tutorial

A tutorial about writing really basic tests

LICENCE

Copyright 2002 Adrian Howard, All Rights Reserved.

This documentation is free; you can redistribute it and/or modify it under the same terms as Perl itself.