For example, there is a standard Config utility class that loads a property file and then provides getter methods for the values.
- Hurdle #1: The Utility class only expects a ServletContext as a means to getting the file name to load.
- Hurdle #2: Various path manipulations have been hard-coded in the file loading method as well.
- Hurdle #3: The internal properties object holding the values is private accessor, designed to be read only by client classes.
- Hurdle #4: Set up for ease of use with static method fields, so I can't just create an instance of my own or subclass easily.
In this project's case, I agree that the access to the config needs to be read only, as only bugs and mischief would result if later generations of developers thought that public or protected access to the Properties meant the config was expected to change at run-time. I also like the static behavior as it makes for clean code with no extraneous getInstance() or setConfig() calls everywhere the class is used.
On the other hand, I am simply too lazy to enjoy having to 1) set up my local environment to exactly match server paths. 2) build and deploy my code to test every code change and 3) stop and start the server to test every property permutation.
So, the challenge with this legacy code was to figure out how to inject properties at Test time without any impact on the existing code. Thankfully in Java I have Reflection and EasyMock.
First - a snapshot of the config class:
public class AppConfig {
private static Properties applicationProperties;
public static void loadConfig(ServletContext context){
... load files into appProperites
//don't worry about this being static on
//a constructed object it has been working over a year now..
}
public static String getProperty(String propKey) {
return appProperties.getProperty(propKey);
}
}
However, good isolated unit tests require I bypass all dependencies on file loading etc. so the first trick is abusing reflection to give me a point to inject a config mock object. Then I can create an EasyMock wrapper on the Properties object to inject which ever specific property and value my client code will need.
// written as a junit method for simplicity here
import org.easyMock.classextension.*;
public void testSomeObject() {
AppConfig env = new AppConfig();
Class c = env.getClass();
try {
Field appProps = c.getDeclaredField("applicationProperties");
appProps.setAccessible(true);
Properties overRideProps = EasyMock.createMock(Properties.class);
EasyMock.expect(overRideProperties.getProperty("testkey")).andReturn("false"); //sets up our test property
EasyMock.replay(overRideProps); // readies mock object to act during test.
appProps.set(env, overRideProps); //puts our mock back into AppConfig object
} catch (SecurityException e1) {
fail ("denied access to reset applicationProperties to public " + e.toString());
} catch (NoSuchFieldException e2) {
fail ("applicationProperties field name changed " + e.toString());
} catch (IllegalAccessException e3) {
fail ( "Unable to override properties in AppConfig " + e.toString());
} catch (IllegalArgumentException e4) {
fail ( "Unable to override properties in AppConfig " + e.toString());
//Note that you might see this exception if you import wrong form of EasyMock must use classextension
}
SomeObject so = new SomeObject();
SomeState testState = new SomeState();
... setup testState ...
String testResult = so.doSomeMethod(testState);
// If the above method does not call the expected property test will fail with EasyMock exception.
// otherwise it will fail if the following assertion fails.
assertEquals( "expectedResult", testResult);
}
Finally, a few words of caution on this style of testing
This test is brittle, it has knowledge of internal code working that would be better it not have. Now neither the Properties field name in AppConfig, nor the property key String can change without breaking this code. So there is a risk that a future developer will curse my name for requiring him to fix test cases for no good reason. If this were not Stable Legacy Code I would be more concerned about this risk.
This test case doesn't guarantee the system will behave as expected once deployed. There are no protections against keys changing in the property file used to load the AppConfig causing runtime defects. So some form of integration testing is needed to ensure the overall system works as intended later.
EasyMock may seem like overkill here, as I could just as easily injected a plain old Properties object. The benefit of EasyMock is that I can see exactly where the test fails and why - wrong key is used or no check is made. For all other reasons the final assert may fail I've got to go in and spend time debugging.
No comments:
Post a Comment