Whenever I need to add a new feature to some legacy code, I attempt to add some tests around the code before I start making changes. This ensures that I don’t break the original purpose of the code. This is not always easy though as some classes and methods can be difficult to get into a test harness. One example I will talk about now is that of the Irritating Parameter which was pointed out to me in the book, Working Effectively with Legacy Code. If you haven’t read this book, I thoroughly recommend it as it explains ways of breaking dependencies to get classes under test, which I have found can be difficult when working with legacy code and large systems.
When a concrete class is passed as a variable to a method, or a constructor, it can make it very difficult to get the method/class under test. Take the following example:
public class CreditValidator{
public CreditValidator(JDBCConnection c, String u, String p) {
Properties props = new Properties();
props.put("user", u);
props.put("password", p);
c.setupConnection(DB_URL, props);
}
...
}
If we want to add a method to this class, then we need to make sure we add tests so we don’t break any current functionality. To do this though, we will have to instantiate the CreditValidator
in a test environment. The problem is the constructor takes in a JDBCConnection
object that is used to connect to a database that is not available to our test environment. As you may have guessed, that object is the Irritating Parameter.
Mocking Frameworks
You may be thinking a mocking framework, such as EasyMock
, would come in handy here as it would allow you to mock the JDBCConnection
object and therefore bypass the setupConnection()
method with ease. Well, you are correct, that would work, but it is not the best thing to do. If you are not in a massive hurry, due to the inevitable looming deadlines we all have, then it would be better to improve the design with some basic refactoring.
The problem with this class is that it is tightly coupled to the JDBCConnection
object, which is a concrete class. This means the CreditValidator
class must always connect to a database using a JDBCConnection
. If, in the future, your boss wants to change the validation of credit so that it uses a different type of connection, then the CreditValidator
is going to have to change (along with all the tests that should now be in place).
Extract Interface
A better solution would be to replace the JDBCConnection
with an interface and then pass this into the constructor instead. This would mean the CreditValidator
is coupled to an interface and not a specific implementation, which would make it loosely coupled. Your development IDE should have a way to do this for you easily, e.g. in Eclipse, you click Refactor in the menu bar and then Extract Interface and follow the wizard. So once we have done this, the code would look like this:
public interface Connection {
public void setupConnection(String url, Properties props);
}
public class JDBCConnection implements Connection {
@Override
public void setupConnection(String url, Properties props) {
}
}
public class CreditValidator{
public CreditValidator(Connection c, String u, String p) {
Properties props = new Properties();
props.put("user", u);
props.put("password", p);
c.setupConnection(DB_URL, props);
}
...
}
Fake Object
Once you have created an interface, you will be able to create a fake object to pass into the CreditValidator
constructor for your tests. To do this, create a FakeConnection
class like this:
public class FakeConnection implements Connection {
@Override
public void setupConnection(String url, Properties props) {
return;
}
}
In your tests, you will easily be able to instantiate a CreditValidator
now and know that setupConnection
will not attempt to connect to a database. You have also cleaned up the design so that it is easier to add new features in the future.
There Is No Silver Bullet
Sometimes, you cannot extract an interface because the class of the parameter passed in is coming from a library. Take the DynamoHttpServletResponse
object in ATG, for example – ATG is an ecommerce framework. This is passed into all ATG servlets and is a concrete class. We cannot create an interface to pass in so we have to come up with another solution to get our method into a test harness. Sometimes, we cannot improve the design but we still need to test the code. In this instance, we can use a mocking framework or we could just try passing null
.
Pass Null
public boolean invokeDefaultLogin(DynamoHttpServletRequest request,
DynamoHttpServletResponse response, String email,
String password) throws ServletException, IOException {
ProfileFormHandler pfh = getProfileFormHandler();
boolean loggedIn = false;
setCredentials(email, password);
pfh.setLoginSuccessURL("/");
boolean retVal = pfh.handleLogin(request, response);
if (!pfh.getFormErrors()) {
loggedIn = true;
}
return loggedIn;
}
In the code above, we can pass null
as the request and the response as they aren’t being used in the method. However, they are being passed into the pfh.handleLogin()
method and that means we should create a FakeProfileFormHandler
and override handleLogin
so that it returns true
or false
based on your test.
Extend and Override
Another technique that can be used to get the CreditValidator
under test is called extend and override. With this technique, you create a new class that extends JDBCConnection
and then you override the setupConnection
method so that it just returns. In your test, you can now pass in your new class to the CreditValidator
constructor and no database connection will be created.
Conclusion
The above examples are only trivial but you should take away from this that there are many ways to get classes/methods into a test harness when there is an irritating parameter. Ideally, you should clean up the design to make it more reusable and easier to maintain in the future but this is not always possible so using a mocking framework, passing null
or using the extend and override technique will at least help you to break the dependency so that you can test your new feature.