Agile Zone is brought to you in partnership with:

John is an experienced consultant specialising in Enterprise Java, Web Development, and Open Source technologies, currently based in Sydney, Australia. Well known in the Java community for his many published articles, and as author of Java Power Tools and Jenkins: The Definitive Guide, and founder of the open source Thucydides Automated Acceptance Test Library project, John helps organisations to optimize their Java development processes and infrastructures and provides training and mentoring in agile development, automated testing practices, continuous integration and delivery, and open source technologies in general. John is the CEO of Wakaleo Consulting, and runs several Training Courses on open source Java development tools and best practices. John is a DZone MVB and is not an employee of DZone and has posted 125 posts at DZone. You can read more from them at their website. View Full User Profile

An Introduction to Test-Driven Development with Legacy code

11.23.2009
| 11408 views |
  • submit to reddit

Test-Driven Development, or TDD, is often quoted as an essential Agile best practice, and so it is. It works wonders on green-fields projects and new code bases where you can start afresh and ensure that all your code is both easily testable and well tested. But what about legacy code? (By legacy code, I mean any code that does not have a comprehensive set of automated tests, so you might be writing legacy code as we speak). For most of us, most of the code we will ever work on will not have originally been our own work. And, unfortunately for the software industry, only a small fraction of code can really boast comprehensive unit and integration tests. How can techniques like Test-Driven Development make our work as developers more productive and less frustrating?

In this series of articles, I'll be looking at Test-Driven Development with legacy code, and refactoring techniques that you can use with legacy code to make your code more testable, and progressively build up a unit test suite for your legacy application.

In fact, Test-Driven Development (along with other related techniques such as Behaviour-Driven Development and Acceptance Test-Driven Development) is a valuable tool for working with legacy code. Test-Driven Development is not just an Agile practice; it is actually just common sense. Would you fly in an airplane where the pilot only ran through the pre-flight check list from time to time, or not at all, and fixed any issues that came up in the air? No, not really. Automated unit tests are like the checklist in a plane - they let you spot problems quickly, automatically, and with little effort on your part. Using Test-Driven Development with existing code is like finding and fixing issues using a pilot's checklist, rather then climbing onto the wing inflight and tightening a bolt or two.

But using Test-Driven Development for legacy code is a little different from using TDD in a green-fields context. The general approach is identical:

  1. Write a test to reproduce the error (test bar red)
  2. Write code to fix the error (test bar green)
  3. and then refactor (test bar still green)
However, when working with legacy code, you might need to do some refactoring upfront to make the code testable in the first place. In legacy code, it can be hard to write a unit test to reproduce a bug or to understand how a particular piece of code works, because it often contains embedded dependencies on other components which are hard (or impossible) to initialize in a unit testing context. Without breaking these dependencies, you have little option but to work with full integration tests. However, if you succeed in isolating the code you need to test from the surrounding components, there is no reason why you can't write effective unit tests to isolate, reproduce and fix bugs in legacy code.

The key here is breaking the dependencies. To effectively test-drive legacy code, you need to be able to add effective unit tests, both to understand how the code works, and to reproduce the issues you are trying to fix. And there are many ways you can do this. These include for example introducing dependency injection to break dependencies and using sub-classes to override key methods that need to be isolated.

But rather than discussing theories, patterns and abstract practices, let's look at an example. Suppose we have a class in our legacy application that builds cars, called CarFactory. Users have complained that there is an error in the serial number being generated for certain models of car.

public class CarFactory {
...
public void buildCars(int number, String model, String brand) {
for(int i = 0; i < number; i++) {
String serialNumber;
//
// 20 lines of code to calculate new serial number
//
Car car = new Car(model,brand,serialNumber);
Car.save(car);
}
}
}

 

The domain class looks like this. The persistence logic is embedded in the save() method. This method opens a JDBC connection and writes to the production database.

public class Car {
private String model;
private String brand;
private String serialNumber;
...
static public void save(Car car) throws PersistenceException {
//
// JDBC code to connect to the production database and to
// write to the T_CAR table
//
}
}

 

We have a pretty good idea that the issue might be in the buildCars() method. But to reproduce this bug in a unit test, we would need to be able to call this method. As it stands, calling this method will write data directly to the database. Furthermore, we don't have a copy of the database locally, as it is too big. So running the method in isolation is out of the question.

Or is it. To do so, we need to be able to stub out the part of the CarFactory that writes to the database, so that we can test the rest. This is made more tricky by the fact that the save() method is static. Let's start by refactoring the CarFactory class slightly, to place the persistence logic in a separate class, called CarDao:

public class CarFactory {

private CarDao dao = new DefaultCarDao();

public void setDao(CarDao dao) {
this.dao = dao;
}

public void buildCars(int number, String model, String brand) {
for(int i = 0; i < number; i++) {
String serialNumber = Integer.toString(i);
Car car = new Car(model,brand,serialNumber);
dao.saveCar(car);
}
}
}

The CarDao interface and DefaultCarDao class could look like this:

public interface CarDao {
public void saveCar(Car car) throws PersistanceException;
}

public class DefaultCarDao implements CarDao {

public void saveCar(Car car) throws PersistanceException {
Car.save(car);
}
}

 

The original CarFactory class is functionally unchanged - by default, it creates a DefaultCarDao object which does exactly the same thing as the original code. However, now we can inject a mocked-out CarDao object for our tests. We have broken the dependency. We can now proceed to write a test that reproduces the issue related to the serial numbers:

public class CarFactoryTest {

@Test
public void carFactoryShouldGenerateTheRightSerialNumber() {
CarFactory factory = new CarFactory();
CarDao mockDao = mock(CarDao.class);
factory.setDao(mockDao);

// I know what the serial number should be
String expectedSerialNumber = "123456";

Car aToyotaPrius = new Car("Toyota", "Prius", expectedSerialNumber);
factory.buildCars(1, "Toyota", "Prius");
verify(mockDao).saveCar(refEq(aToyotaPrius)));

}
}

We can also use this approach to ensure that the CarFactory class is invoking the Car.save() method correctly. In the following test, for example, we check that the buildCars() method calls Car.save() the right number of times, and that it passes through the correct model and brand values:

public class CarFactoryTest {

@Test
public void carFactoryShouldCreateTheRightNumberOfCars() {
CarFactory factory = new CarFactory();
CarDao mockDao = mock(CarDao.class);
factory.setDao(mockDao);

Car aToyotaPrius = new Car("Toyota", "Prius","");
factory.buildCars(10, "Toyota", "Prius");
verify(mockDao, times(10)).saveCar(refEq(aToyotaPrius, "serialNumber"));

}
}

This is just one example of this sort of approach. In future articles, I'll look at other problems with legacy code and other approaches. But in all cases, you'll find that using TDD with legacy code will both help you fix bugs and add new features faster, and progressively improve the overall quality of your code.

If your are working with legacy code, Michael Feather's 'Working Effectively with Legacy Code' contains many valuable tips and tricks in this area. I also cover how to use TDD techniques with legacy code in the upcoming Test and TDD for Java Developers course, which will be running in Sydney and Melbourne in December, and in other sites next year.

References
Published at DZone with permission of John Ferguson Smart, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

Jeff Szeto replied on Mon, 2009/11/23 - 5:16pm

What you suggested is a general good practice of refactoring components that are highly coupled. However, often in many real world cases, legacy code is NOT allowed to be refactored ( too risky to be modified with limited domain knowledge on the code base), a wonderful tool called "Powermock" (http://code.google.com/p/powermock/) allows you to mock things that are considered "untestable". In your example, we could mock the static method "save(Car)" using mockStatic() feature in Powermock.

Rogerio Liesenfeld replied on Mon, 2009/11/23 - 7:05pm

For the curious, here are the JMockit tests for the original, unmodified, CarFactory and Car classes:
public class CarFactoryTest
{
    @Test
    public void carFactoryShouldGenerateTheRightSerialNumber() {
        new MockUp<Car>()
        {
            @Mock(invocations = 1)
            void save(Car car) {
               assertEquals("Toyota", car.getModel());
               assertEquals("Prius", car.getBrand());
               assertEquals("123456", car.getSerialNumber());
            }
        };

        new CarFactory().buildCars(1, "Toyota", "Prius");
    }

    @Test
    public void carFactoryShouldCreateTheRightNumberOfCars() {
        new MockUp<Car>()
        {
            @Mock(invocations = 10)
            void save(Car car) {
               assertEquals("Toyota", car.getModel());
               assertEquals("Prius", car.getBrand());
            }
        };

        new CarFactory().buildCars(10, "Toyota", "Prius");
    }
}

Easy, isn't it?

Mateusz Mrozewski replied on Tue, 2009/11/24 - 2:54am

I wrote a short blog post on mocking static methods: http://tech.mrozewski.pl/2009/11/16/mocking-static-methods-with-powermock/

PowerMock can also be used to override constructors behaviour and init blocks which makes testing legacy code far more easier.

Of course this won't solve the problem of finding and fixing bugs in legacy code, but as Jeff mentioned this is not always allowed.

John Ferguson Smart replied on Wed, 2009/11/25 - 5:50am

PowerMock is indeed a great tool for this sort of thing - I was reserving some examples of how to use it for a future article ;-)

Alessandro Puzielli replied on Wed, 2009/11/25 - 7:48am

Hi john,

I lose a step:  in the method Car.sava(Car car) there's a connection to DB and you says that the DB is unaviable.

Then in the DAO I need a connection to DB (the method saveCar(Car car) call this method without other elaboration).

 When in test this instruction runs:

verify(mockDao, times(10)).saveCar(refEq(aToyotaPrius, "serialNumber"));

you might to receive a PersistentException (or SQLException) in testing time and you can't read the value of  assertion.

How do you can testing the method save if you don't modify its code?

Do you use any framework? In the article there's mention.

 Thanks

 

 

Mauro Jean-françois replied on Thu, 2009/12/10 - 5:51am

@Alessandro I think John is using the Mockito framework to mock the CarDao.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.