wissel.net

Usability - Productivity - Business - The web - Singapore & Twins

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 on 09 December 2021 | Comments (0) | categories: Domino Java

Comments

  1. No comments yet, be the first to comment