I was speaking to a colleague recently during one of our fortnightly tech forums and he mentioned a specific class that he found difficult to get under test. The problem was that the constructor of the class was attempting to instantiate and configure new objects that were difficult to setup in a test harness. When you are faced with a constructor that instantiates classes internally, there are a few different ways to break these dependencies safely. This article describes some of them and the reasons for using each one.
Parameterise Constructor
When confronted with a constructor that instantiates one or two classes internally, then we can use a technique called parameterise constructor to get the class into a test harness. Take the example class below.
public class ForeignExchange() {
private String currency;
private Converter converter;
public ForeignExchange(String currency) {
this.currency = currency;
this.converter = new CurrencyConverter(currency);
}
}
When the ForeignExchange
class gets instantiated, it creates a CurrencyConverter
object, which takes a parameter to determine which currency the converter is for. The problem we have is we don’t know if we can instantiate the ForeignExchange
class as it is dependant on the CurrencyConverter
class being instantiated. So what do we do in this situation? The first thing should always be to try and instantiate it in a test harness. If it fails, you’ll be told why pretty quickly.
So, we’ve tried to instantiate the object in a test harness and it has failed because the CurrencyConverter
constructor calls an external system that we cannot replicate easily in a test harness. We aren’t really bothered about testing the CurrencyConverter
though, as we are only trying to get the ForeignExchange
class into a test harness so we can alter some of the behaviour. Therefore, we want to bypass the CurrencyConverter
object so we can concentrate on our changes. How do we bypass an object that is instantiated in the constructor internally? With parameterise constructor.
This is really simple, all you do is pass in the object you wish to break the dependency on, e.g.:
public ForeignExchange(String currency, <span style="background-color:#fffaa5;">Converter converter</span>) {
this.currency = currency;
this.converter = converter;
}
You then create another constructor to mimic the original one, i.e., passing the original parameters, so that any existing code doesn’t break, e.g.:
public ForeignExchange(String currency) {
this(currency, new CurrencyConverter(currency));
}
Now you can create a test by passing null
as the Converter
to the new constructor or you can mock/fake the class instead.
One downside to this refactoring is that it opens the code up slightly as there are now 2 public
constructors that any developer can use in the future. Remember that this is a technique to break a dependency safely so that you can get your tests in place for the new code you are going to write. We don’t always have the appropriate time to refactor the code in the way we want to but we should ALWAYS get any new code we write under test. This method may not be pretty, and may come with its issues, but it will allow you to test your code, and therefore, I think it is a worthy trade-off in some situations.
Extract and Override Factory Method
When a constructor attempts to instantiate a few objects, the parameterise constructor technique is not the most ideal method for getting the class under test. The reason for this is because the parameter list for the constructor can become too large. Generally, if there are more than 3 parameters in a method signature, you should be thinking of ways to refactor the method. So, what do we do with the method below:
public class NavigationAccessor {
private Navigation navigation;
public NavigationAccessor() {
Site site = SiteContextManager.getCurrentSite();
List<NavigationFacet> facets = new ArrayList<NavigationFacet>();
NavigationItems items = site.getNavItems();
for (NavigationItem item : items) {
NavigationFacet facet = new NavigationFacet(item);
facets.add(facet);
}
TopNav topNav = new TopNav(items);
LeftNav leftNav = new LeftNav(items);
navigation = new Navigation(site, facets, topNav, leftNav);
}
}
This constructor uses a static
class to get a site and then creates a list of NavigationFacets
, a TopNav
object, a LeftNav
object and a Navigation
object – that’s a lot of work to instantiate a NavigationAccessor
. We can’t use parameterise constructor because we’d have to pass in 4 parameters. So instead, we can extract the method and then create a new class that extends and overrides the object creation.
In the example above, all of the code helps to instantiate a Navigation
object. Therefore, we first extract it all out to a new method called createNavigation()
using the ‘Extract Method’ function of your IDE.
public class NavigationAccessor() {
private Navigation navigation;
public NavigationAccessor() {
navigation = createNavigation();
}
protected Navigation createNavigation() {
Site site = SiteContextManager.getCurrentSite();
List<NavigationFacet> facets = new ArrayList<NavigationFacet>();
NavigationItems items = site.getNavItems();
for (NavigationItem item : items) {
NavigationFacet facet = new NavigationFacet(item);
facets.add(facet);
}
TopNav topNav = new TopNav(items);
LeftNav leftNav = new LeftNav(items);
return new Navigation(site, facets, topNav, leftNav);
}
}
I’ve made the createNavigation()
method protected
because we will need to override this for our tests. Again, this is bad design because this opens this method up to being extended or overridden. However, although we wouldn’t choose to do this normally, this is a tool to get legacy code under test safely so that we can add our new code without worrying that we are breaking the existing functionality. Therefore, it may be a necessary evil.
The next step is to create a new class that extends the NavigationAccessor
then override the createNavigation()
method to return a FakeNavigation
object (or null
, if not required).
public class FakeNavigationAccessor extends NavigationAccessor {
@Override
protected Navigation createNavigation() {
return null;
}
}
Now we can instantiate a FakeNavigationAccessor
class in our test harness and the constructor will set the ‘navigation
’ object to null
. This means we can concentrate on getting our tests in for our new code safely.
Conclusion
Neither of these methods generated particular great design. The reasons to use these techniques are to help you get some legacy code into a test harness in a safe way. Ideally, you would put tests around all of the code and then refactor it to make the design better but we don’t always have the time to do this. Instead, you can use these methods to safely alter the code, without writing unit tests, so that you can get the class instantiated and, therefore, can test your new code. The key point of these techniques is to get your new code tested. If you have some spare time after you have finished your code, then you can always go back and try to improve the design and remove some of the nasties that were introduced.