Agile, test-driven development

It All Started with a Test

Author(s):

Test-driven development with a full-coverage regression test suite as a useful side effect promises code with fewer errors. Mike "Perlmeister" Schilli enters the same path of agility and encounters a really useful new CPAN module.

A few weeks ago, my employer sent me to a course about test-driven development (TDD) and agile methods, and to implement my newly gained knowledge practically, I am dedicating today's Perl snapshot to this principle.

Fast and flexible developers typically set off immediately without paying attention to details. They always write a test first before they get down to implementing a function. The test suite thus grows automatically with tests relevant to system functions. They clean up dirty code by refactoring later; this is possible without any risk thanks to the safety net provided by the test suite.

Nothing but Errors – and Rightly So

The tests developed before writing a function will fail, of course, because the desired feature either does not exist or is only partially or incorrectly implemented at first. When the code arrives later on, the test suite passes and turns to green; development environments such as Eclipse actually visualize it this way.

For example, to write a User.pm class for a login system that later supports methods such as login(), TDD disciples first create a test case. It checks whether the desired class can actually be instantiated. Listing 1 shows the test file for simple test cases, Basic.pm. It is located in the directory t and uses the brand new CPAN Test::Class::Moose module from CPAN. The latter runs all methods with the test_ prefix and the test utility routines they invoke.

Listing 1

Basic.pm

 

Listing 1 [1] defines test_constructor() and runs the command

can_ok 'User', 'new';

from the Test::More module in it. Thus, test_constructor() verifies whether the User class is capable of calling its constructor new.

Listing 2 shows a script for running the test suite. At first, it draws in the Load module, which loads all Perl modules with the .pm suffix that exist in the specified subdirectories (. and t). The runtests() method then executes all test_* routines found in these modules. In this phase of the project, the User class does not yet exist, and the test case in test_constructor() thus fails (Figure 1).

Listing 2

runtests

 

Figure 1: Initially, the test suite raises the alarm because nobody has written the User class yet.

A Sense of Achievement

The test-driven developer naturally expected this outcome and now does everything possible to add code until the test suite completes without error. Because the class does not exist, the developer creates a new User.pm file and then adds the following content:

package User;
use Moose;
1;

Old hands like your very own Perlmeister might rub their eyes in disbelief at this, because the User package does not define a new() constructor that welds an object hash $self onto a package using bless(). The Moose [2] CPAN module does all of this behind the scenes, so every packet that Moose looks at automatically owns a new() constructor.

A new run of the test suite using ./runtests returns these promising results:

<[...]>
ok 1 - TestsFor::User

The suite therefore finds the new .pm file, the class it contains, and the new() constructor.

Once More into the Agile Fray

Green light – the signal for TDD developers to add a new feature. The User object needs methods to set and query the user's email address. A developer working conventionally would probably immediately start typing the seemingly simple code. Not so TDD followers; they first write a test that again fails.

Listing 3 defines the test_accessors() method, which the test module later also finds and calls due to the prefix. It creates a new object of the User type and passes the parameter pair email => 'a@b.com' to the constructor. One line later, the erstwhile undefined accessor retrieves the email string set by the constructor, and the is function from the Test::More module compares the value with the one set previously. If the contents match, is writes the ok string to the TAP output of the test suite. The suite will recognize this as a successfully executed test case.

Listing 3

Accessors.pm

 

This test is followed by a test of the setter, which uses the email() method to set a new value for the user's email address and then runs the accessor (also email() but without an argument) to retrieve the stored value again and compare it with the original. But the User.pm class still does not have the necessary code; the new test therefore fails immediately.

Getters and Setters

For the constructor of the User class to accept the email string parameter as a named parameter, an eponymous accessor method to give it back, and a setter to set new values, Perl hackers had to insert dozens of lines of code manually in the pre-Moose era. With Moose, this is a no-brainer because its has function defines a class attribute that can, at the same time, be addressed using a constructor parameter, a getter – email() – and a setter – email( $ email ).

Listing 4 shows a later version of the User class that uses has to define the email attribute. Its is parameter uses rw to make the value readable and writable; isa defines it as a Str (i.e., an arbitrary string).

Listing 4

User.pm

 

Running the test suite again in Figure 2 shows that all three defined test cases now complete successfully. So, development can proceed.

Figure 2: Green light: The test suite runs without error; agile development can now proceed.

Putting the Customers in a Database

The next thing the product development specification requires is that users register in a customer database using their email addresses. True to the principles of TDD, Listing 5 defines the first test case with the test_customers() routine. It uses the Customers class and its new() constructor to generate a new in-memory customer database.

Listing 5

Register.pm

 

Next, it feeds two new users with their email addresses to the database using the not yet existing sign_up() method. In the second for loop starting in line 19, the test routine uses ok and the method user_find_by_email() to check whether the customer file object can find the recently registered customers. In this case, the method will return a true value by definition, once it has been implemented.

Searching for Users

Again, all grinds to a halt if the failed test suite wants it to be that way. To fix the "bug," Listing 6 implements the Customers class, again using Moose and two additional methods. Perl's object system passes in a reference to the object as the first argument in method calls. The class defines a global hash %USERS in which the sign_up() method stores the User type object passed to it under the user's email address.

Listing 6

Customers.pm

 

The lookup method user_find_by_email() calls exists to check the global hash and returns either the user object it finds, if the user is already registered, or undef if it does not find the user. Once the code in Listing 6 is free of errors, the green light comes on again, and a further milestone in the project is in the bag.

Moose – Found Without Searching

The CPAN Test::Class::Moose module is still in beta; I only learned of its existence at the Perl YAPC conference [3] in early June in Austin, Texas – just a few hours before deadline for this issue. My first impression is that it is very stable; however, if you find any bugs, the author welcomes reports or patches for the module.

The advantage of test-driven development is undoubtedly the ever-growing test suite, which – if the developer works to plan – provides virtually 100 percent code coverage. If the customer suddenly springs change requests on you in the course of the project, TDD enthusiasts can adopt them without any worries, as the test suite guarantees that their adoption won't introduce fatal errors to previously working parts of the code.

Agile developers also need not worry too much about the most elegant way to implement a specific feature. A straightforward approach is good enough for the time being, and once the test suite shows green, they can move on to the next feature.

After spending a certain time in rapid development, you will naturally come across ugly pieces of code that need to be fixed every few iterations to make the software maintainable: If you find a duplicated piece of code, it can usually be swapped out into a function, and if parts of the system turn out to be known software patterns, you will want to convert them to their reference implementations.

This refactoring is a natural part of the process and does not usually cause any problems – again, thanks to the existing test suite and its extensive code coverage. If the test suite shows you a green light, your spring cleaning was successful.

The Author

Mike Schilli works as a software engineer with Yahoo! in Sunnyvale, California. He can be contacted at mailto:mschilli@perlmeister.com. Mike's homepage can be found at http://perlmeister.com.