JaVa
   

How Does the Test Get to the Mock?

In all our previous examples, we had little trouble foisting the dummy or mock object on the test object. While the interface of the valueInEuro() method passed ExchangeRateProvider in our Euro calculator example, LogServer was passed to Logger within the constructor in our logger example. The choice between the two options depends on different points:

Either option lets us simply replace a helper or server object with dummy or mock objects. However, most existing programs were written without testing requirements in mind; that is, objects used internally are hardwired. The required helper objects are often created and maintained in instance variables during the initialization of an object. It is relatively easy to make such objects testable by offering additional methods to replace these helpers. Our EuroCalculator would then look like this:

public class EuroCalculator {
 private ExchangeRateProvider provider =
 new ExchangeRateProvider();
 public void setProvider(ExchangeRateProvider newProvider) {
 provider = newProvider;
 }
 double valueInEuro(double amount, String currency) {...}
}


Accordingly, our test class has to explicitly replace the correct provider with a dummy provider in the test methods:

public void testUSD2EUR() {
 ExchangeRateProvider dummyProvider = new DummyProvider(1.1324);
 EuroCalculator calculator = new EuroCalculator();
 calculator.setProvider(dummyProvider);
 double result = calculator.valueInEuro(1.5, "USD");
 assertEquals(1.6986, result, ACCURACY);
}


We can see that the test is now longer and harder to read. In addition, there is a risk of forgetting to replace another component with its mock counterpart in more complex test scenarios. This can lead to subtle failures or errors in the test that will be hard to discover. However, we now have the benefit that the app code does not have to know anything about the replacement of a provider object. Testing by use of dummies is even more difficult if a new server object is created in each place it is used, for example, to avoid synchronization problems. Our (simplified) valueInEuro() method would then look like the following:

public double valueInEuro(double amount, String currency) {
 ExchangeRateProvider provider = new ExchangeRateProvider();
 double exchangeRate = provider.getRateFromTo(currency, "EUR");
 return amount * exchangeRate;
}



In this case, our constructor invocation new ExchangeRateProvider() is nothing but an implicit constant, playing the same ugly role when the software is maintained and tested. If we change the constants, we need to search for all places they occur throughout the code—a first step towards the "maintenance trap." Being determined to properly test such a method, we have to do major restructuring work: the ExchangeRateProvider has to be structured so that it can be replaced. If we do not want to pass the provider as an additional parameter, there is only one last trick left: instead of the instance itself, we pass a factory object. When testing, we can replace the factory object by a mock factory. To this end, we need an interface with two implementations:

public interface ProviderFactory {
 public ExchangeRateProvider createProvider();
}
public class RealProviderFactory implements ProviderFactory {
 public ExchangeRateProvider createProvider() {
 return new ExchangeRateProvider();
 }
}
public class MockProviderFactory implements ProviderFactory {
 private double rate;
 public MockProviderFactory(double rate) {
 this.rate = rate;
 }
 public ExchangeRateProvider createProvider() {
 return new DummyProvider(rate);
 }
}


Now we can either pass the appropriate factory object in the EuroCalculator class with the constructor or replace it by using a setter method. The following would be a typical case:

public void testUSD2EUR() {
 ProviderFactory factory = new MockProviderFactory(1.1324);
 EuroCalculator calculator = new EuroCalculator(factory);
 double result = calculator.valueInEuro(1.5, "USD");
 assertEquals(1.6986, result, ACCURACY);
}



However, the testability here is at the cost of simplicity and readability. The additional detour is harder to understand than a straight constructor invocation. To our advantage, though, we win independence of the EuroCalculator from a specific ExchangeRateProvider.


JaVa
   
Comments