Factory based dependency injection
No man is an island and no code you write lives without dependencies (even your low-level assembly code depends on the processor's microcode). Testing (with) dependencies can be [insert expletive]
Dependency injection to the rescue
The general approach to make dependent code testable is Dependency injection. Instead of calling out and create an instance of the dependency, the dependency is hand over as parameter. This could be in a constructor, a property setter or as method parameter.
A key requirement for successful dependency injection: the injected object gets injected as an Interface rather than a concrete class. So do your homework and build your apps around interfaces.
An example to illustrate how not to do, and how to change:
public Optional<Customer> findCustomer(final String id) {
// Some processing here, omitted for clarity
// actual find
final CustomerDbFind find = CustomerDb.getFinder();
return Optional.ofNullable(find.customerById(id));
}
When you try to test this function, you depend on the static method of the CustomerDb
which is a pain to mock out. So one consideration could be to hand the CustomerDb
as dependency. But this would violate "provide interface, not class". The conclusion, presuming CustomerDbFind
is an interface will be:
public Optional<Customer> findCustomer(final CustomerDbFind find, final String id) {
// Some processing here, omitted for clarity
// actual find
return Optional.ofNullable(find.customerById(id));
}
This now allows to construct the dependency outside the method to test by implementing the interface or using a Mock library
Not so fast
Handing over all dependencies as method parameters can lead to parameter bloat and prevent methods to be used in functional interfaces, which by design don't allow adding additional parameters.
The solution we came up with, is to use a factory interface to provide all those global values (setup, flags, pools etc) in an default implemented interface that is retrieved from a Singleton. Bear with me on the details:
// The interface we get global objects from
public interface GlobalFactory() {
default CustomerDbFind getFinder() {
return CustomerDb.getFinder();
}
// Many more
}
public enum GlobalFactorySource() {
INSTANCE;
// Note the curly brackets at the end
private GlobalFactory gf = new GlobalFactory() {};
public GlobalFactory get() {
return this.gf;
}
public void overWrite(final GlobalFactory overwrite) {
this.gf = overwrite;
}
public void reset() {
this.gf = new GlobalFactory() {};
}
}
Now we can refactor our method to be compliant with functional interfaces (one in, one out):
public Optional<Customer> findCustomer(final String id) {
// Some processing here, omitted for clarity
// actual find
final CustomerDbFind find = GlobalFactorySource.INSTANCE.get().getFinder();
return Optional.ofNullable(find.customerById(id));
}
This now can be used in Java streams like this:
List<Customer> customers = customerIdList.stream()
.map(this::findCustomer)
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
Onwards to testing
In your unit test (in BeforeAll) you would overwrite the factory with something constructed or mocked:
GlobalFactory mockFactory = new GlobalFactory() {
@Overwrite
CustomerDbFind getFinder() {
return new MockFinder(); // Or Mockito.mock(....)
}
// Only overwrite the things you need to mock
}
GlobalFactorySource.INSTANCE.overWrite(mockFactory);
// Test code and all goes here
Assertion.assertTrue(findCustomer.isPresent());
// At the end
GlobalFactorySource.INSTANCE.reset();
As usual YMMV
Posted by Stephan H Wissel on 09 December 2021 | Comments (0) | categories: Domino Java