Cameron Hotchkies

Categories

  • Coding

Tags

  • development
  • dependency-injection
  • refactoring
  • testing

If you have a kitchen, and that kitchen has drawers, there is a roughly 100% chance you have a junk drawer. You know the one, where you store old phone cables, ribbons, safety pins, expired coupons, and a broken red crayon. The software equivalents of these junk drawers take many forms, commonly named Helper, Utils or the worst offender of all.. the TestBase. Join me as we take our new found love of dependency injection, and actually put it to good use.

The dreaded TestBase

This is something that becomes more prevalent above the unit test layer and more common in an integration test layer. At a certain point environmental resources, external state, and other moving parts are going to need to be initialized for tests to validate specific scenarios. After a few methods repeating the same code, the TestBaseOfUnusualSize is created.

It definitely makes sense to minimize code boilerplate when generating tests in the first place, but it comes at a cost of context to later readers. As systems grow and become more complex, different portions will only have partially overlapping resource requirements. Eventually the system ends up with a test base that is this thorny tentacled monster that just works, as long as you don’t look too hard at it.

public class ArcticLairTest extends SuperVillainTestBase {

  @Test
  public void ensureLairKeepsFoodCold() {
    // the provider creation will differ based on your DI framework
    // so we'll hand-wave that away atm
    ArcticLair arcticLair = new ArcticLair(
      mock(TrapDoorContractor.class),
      mock(HvacContractor.class),
      super.foodDeliveryService,
      true
    );

    arcticLair.requestFoodDelivery();
    int atDeliveryTemp = arcticLair.getTemperatureOfFood();
    assertCloseTo(295, atDeliveryTemp);

    arcticLair.timeTravel(1000 * 60 * 60 * 8);

    int secondMeasurement = arcticLair.getTemperatureOfFood();
    assertLessThan(278, atDeliveryTemp);
  }
}
public class SuperVillainTestBase {

  protected UmbrellaSupplier umbrellaSupplier;
  protected HedgeMazeDesigner hedgeMazeDesigner;

  protected FoodDeliveryService foodDeliveryService;

  @Before
  public void setUp() {
    this.umbrellaSupplier = new UmbrellaSupplier(
      400,
      19.43,
      CreditCardProcessor.getInstance()
    );

    this.hedgeMazeDesigner = new HedgeMazeDesigner();
    this.hedgeMazeDesigner.setHedges(Yew.class);
    this.hedgeMazeDesigner.growForest();

    this.foodDeliveryService = new FoodDeliveryService();
    this.foodDeliveryService.addStock(Banana.class, 100);
    this.foodDeliveryService.addStock(Peas.class, 100);
    this.foodDeliveryService.addStock(Pudding.class, 400);
  }
}

Dependency injection has a purpose

This is where dependency injection refactoring will help. Ultimately, these large test bases can be replaced with a module, or several, that define the requirements for a test suite. Instead of spending every test suite run initializing a requirement used by only 5% of unit tests, the module can omit or mock unimportant subsystems. In the previous example we had to wait for a forest to grow for every test suite even though everyone knows a hedge maze in the arctic would be made with Boxwood!

The first step is to realize that the FoodDeliveryService initialization could be done internal to a subclass that lives in the test package.

public StockedFoodDeliveryService() extends FoodDeliveryService {
  @Inject()
  public StockedFoodDeliveryService() {

    this.addStock(Banana.class, 100);
    this.addStock(Peas.class, 100);
    this.addStock(Pudding.class, 400);
  }
}
public class ArcticLairTest {

  public static class Module {
    @Provides
    public FoodDeliveryService(Provider<StockedFoodDeliveryService> provider) {
      provider.get();
    }
  }

  @Test
  public void ensureLairKeepsFoodCold() {
    // the provider creation will differ based on your DI framework
    // so we'll hand-wave that away atm
    ArcticLair arcticLair = new ArcticLair(
      mock(TrapDoorContractor.class),
      mock(HvacContractor.class),
      this.foodDeliveryServiceProvider.get(),
      true
    );

    arcticLair.requestFoodDelivery();
    int atDeliveryTemp = arcticLair.getTemperatureOfFood();
    assertCloseTo(295, atDeliveryTemp);

    arcticLair.timeTravel(1000 * 60 * 60 * 8);

    int secondMeasurement = arcticLair.getTemperatureOfFood();
    assertLessThan(278, atDeliveryTemp);
  }
}

The end of magic

By extracting a reusable FoodDeliveryService subclass, we still have the reusable code from the test base. We have also created a visible area of the test suite that explicitly defines all of the expectations of system state. No more magical setup, and hopefully a reduction in heisenbugs.

Another indicator that this form of refactoring may be applicable is the existence of the @TestOnly annotation. If there is a class that has this feature, could a subclass solve the same requirement? Is the @TestOnly method resetting a singleton’s internal state as opposed to just generating a fresh object graph? Don’t be afraid of test only subclasses, they function as documentation of expected states and allow for more composition friendly test suites.

Removal of @TestOnly methods make for quick, short PRs that can speed up other refactoring tasks. Remember, keep your refactoring changes on the small side for faster reviews and more stable releases.