This extension of the factory pattern has the ability to to create a factory of related objects without the classes of those objects needing to be explicitly expressed. This is a work in progress; there is a tiny amount of reflection in my implementation that I do not know how to work around. There are also some ugly coercions, warned of by IntelliJ IDEA, and as noted by comments below.
The purpose was to be able to test some code that consumed Consul* through an API. That an instance of Consul must run while JUnit tests were executed was unacceptable. There were other constraints on how I could intercede.
* See Consul by HashiCorp is an amazing, clusterable key-value store.
import org.junit.Test; public class StoreTest { @Test public void testWithDefaultConsul() { System.out.println( "- testWithDefaultConsul --------------------------" ); //noinspection unchecked StoreFactory.setDefaultStore( ( Class ) ConsulStore.class ); StoreFactory factory = new StoreFactory(); Store store = factory.createStore(); System.out.println( store.toString() ); } @Test public void testWithMock() { System.out.println( "- testWithMock -----------------------------------" ); //noinspection unchecked StoreFactory.setDefaultStore( ( Class ) MockStore.class ); StoreFactory factory = new StoreFactory(); Store store = factory.createStore(); System.out.println( store.toString() ); } @Test public void testConsulStoreInNestedCall() { System.out.println( "- testConsulStoreInNestedCall --------------------" ); //noinspection unchecked StoreFactory.setDefaultStore( ( Class ) ConsulStore.class ); UseStore user = new UseStore(); user.method(); } @Test public void testMockedStoreInNestedCall() { System.out.println( "- testMockedStoreInNestedCall --------------------" ); //noinspection unchecked StoreFactory.setDefaultStore( ( Class ) MockStore.class ); UseStore user = new UseStore(); user.method(); } }
These are the operations that our store must accomplish.
public abstract class Store { public abstract Store initialize(); public abstract String getValue( String key ); public abstract void setValue( String key, String value ); public abstract void delete( String key ); }
We're just proving the concept: we're showing no actual implementation.
Imagine establishing a client connection to Consul here (not shown). Not doing anything that would result in that from JUnit tests is why we've created this class hierarchy.
public class ConsulStore extends Store { @Override public Store initialize() { return this; } @Override public String toString() { return "This is Consul store!"; } @Override public String getValue( String key ) { return null; } @Override public void setValue( String key, String value ) { } @Override public void delete( String key ) { } }
Like the real thing, this mocked store supports all the same operations, but it won't need a filesystem or shake hands with an external service. Of course, there isn't going to be an implementation exposed here either: this code serves only as proof of concept, but in tests, we would probably mock it and use Mockito when clauses to instruct the test what to return. Or, we could just make it "real" using a Java map implementation.
This class does not reside in production, but under the test aspect.
public class MockStore extends Store { @Override public Store initialize() { return this; } @Override public String toString() { return "This is the mocked store!"; } @Override public String getValue( String key ) { return null; } @Override public void setValue( String key, String value ) { } @Override public void delete( String key ) { } }
public abstract class AbstractStoreFactory { public abstract Store createStore(); }
This is the only class in this solution where there's reflective hanky panky. The point is simply to avoid having to include MockStore in production code.
public class StoreFactory extends AbstractStoreFactory { @SuppressWarnings( "unchecked" ) private static Class< Store > defaultStore = ( Class ) ConsulStore.class; public static void setDefaultStore( Class< Store > store ) { defaultStore = store; } public Store createStore() { Store store; try { store = defaultStore.getDeclaredConstructor().newInstance(); // (reflection here) return store.initialize(); } catch( InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e ) { e.printStackTrace(); } return null; } }
The code that uses the store.
This is the fly in the ointment: tests executed code arbitrarily deep in the application that eventually led to establishing a connection with Consul that we wished no longer to allow. This simulates an "arbitrary amount" of depth.
public class UseStore { private Store store; public UseStore() { StoreFactory factory = new StoreFactory(); store = factory.createStore(); } public void method() { System.out.println( store.toString() ); } }
Output:
- testWithDefaultConsul ----------------------------------------------------------------- This is Consul store! - testWithMock -------------------------------------------------------------------------- This is the mocked store! - testConsulStoreInNestedCall ----------------------------------------------------------- This is Consul store! - testMockedStoreInNestedCall ----------------------------------------------------------- This is the mocked store!