Dependency Injection mit ParameterResolver in JUnit 5

JUnit 5 bieten eine ganze Reihe neuer Möglichkeiten um Unit Tests kompakt und strukturiert zu schreiben. Unter anderen gibt es nun die Möglichkeiten die Test Methoden mit Parametern zu versehen. Genauer gesagt können Konstruktoren und alle Methoden, die mit @Test@RepeatedTest@ParameterizedTest@TestFactory@BeforeEach@AfterEach@BeforeAll oder @AfterAll annotiert sind, um Parameter ergänzt werden.

Die Parameter werden je nach Typ beim Aufruf der Methoden mit den entsprechenden Werten belegt.

@Test @DisplayName("display name") @Tag("tag")
void test1(TestInfo info) {
  assertEquals("display name", info.getDisplayName());
  assertTrue(info.getTags().contains("tag"));
}

Hier in diesem Beispiel wird der Parameter info mit einer TestInfo Instanz belegt. Die aktuellen Werte werden von einem in JUnit 5 bereitgestellten ParameterResolver befüllt. Entwickler können aber auch eigene ParameterResolver implementieren.

Ein einfaches Beispiel für die Verwendung eines ParameterResolvers ist das Bereitstellen eines JsonNodes aus einer Datei für die Tests. Um die Aufgabe etwas anspruchsvoller zu gestalten, soll es für jeden Test eine andere Datei sein. Selbstverständlich kann man dieses Problem auch schon mit älteren JUnit Versionen lösen.

private JsonNode load(String resource) {
  return new ObjectMapper().readTree(getClass().getResourceAsStream(resource));
}

Diese Methode kann man nun in jedem Test aufrufen. Sie existiert in der speziellen Testklasse oder verschwindet in einer von vielen Utility Klassen. Die Wiederverwendung dieser Methode in einem anderen Kontext ist nicht sehr wahrscheinlich.

class Test4 {
  @Test
  public void testWithJUnit4() {
    JsonNode node = load("junit4.json");
    assertEquals("JUnit 4", node.textValue()); 
  }
}

Mit JUnit 5 und eigenem ParameterResolver kann der Test kompakter geschrieben werden.

@ExtendWith(JsonNodeParameterResolver.class)
class Test5 {
  @Test 
  public void testWithJUnit5(@Resource("junit5.json") JsonNode node) {
    assertEquals("JUnit 5", node.textValue()); 
  }
}

Dazu erstellen wir einen eigenen ParameterResolver für Parameter vom Typ JsonNode und eine Annotation @Resource, die den Namen der Quelldatei bereitstellt. Der ParameterResolver sorgt auch dafür, dass die benötigten Objekte nur für diesen speziellen Test erzeugt werden. Im Gegensatz zur Setup Methode, die häufig Objekte erzeugt, die im jeweiligen Test nicht benötigt werden.

public class JsonNodeParameterResolver implements ParameterResolver {
  @Override
  public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
    return parameterContext.getParameter().getType().equals(JsonNode.class);
  }
  @Override
  public ObjectNode resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException {
    Resource annotation = findAnnotation(parameterContext.getParameter(), Resource.class);
    if (annotation == null) {
      throw new ParameterResolutionException("no @Resource annotation found");
    }
    String resource = annotation.value();
    try {
      return (ObjectNode) new ObjectMapper().readTree(extensionContext.getTestClass().get().getResourceAsStream(resource));
    } catch (IOException e) {
      throw new ParameterResolutionException("could not load Json from " + resource, e);
    }
  }
}

So bekommen wir nicht nur eine saubere Trennung zwischen Support- und Test-Code, auch die Wiederverwendbarkeit ist durch einen ParameterResolver verbessert.