TestNG Guice-Annotation in JUnit nutzen

09. Juli 2021 Softwaretest von Eric Kubenka

Schon seit Jahren arbeite ich in java-basierten Testautomatisierungsprojekten ausschließlich mit TestNG als Testframework.
Als ich im Jahr 2014 im Beruf die ersten Projekte umsetzte, konnte TestNG einfach durch ein besseres Feature-Set und eine deutlich bessere parallelisierte Testausführung punkten.

Und auch in der heutigen Zeit, gefällt mir persönlich die Entwicklung von TestNG besser, zum Beispiel auch die Guice-Integration. Mit einer simplen Annotation an einer Testklasse, kann das eigene Guice-Modul geladen und somit Depenency-Injection deutlich vereinfacht werden.

Diesen Komfort gibt es in JUnit (5.7.2) nicht, jedoch lässt es sich mit einfachen Mitteln nachbauen und ich zeige wie.

Entwicklungsumgebung einrichten

Ich arbeite mit Windows 10 und verwende chocolatey als Paketverwaltung, weil es das Reproduzieren von Software-Installationen einfacher macht, weshalb ich kurz darstellen möchte, wie ich meine Entwicklungsumgebung inklusive Java, JDV und Maven aufgesetzt habe.

choco install maven
choco install openjdk--version=16.0.1

Dependency Injection Beispiel

Für die Veranschaulichung und Verwendung habe ich ein einfaches Beispiel gewählt. Es existiert ein Interface StorageInterface, welches von einer oder mehreren Klassen implementiert werden kann. Zur Laufzeit benötige ich in meinem Test einen Objekt-Storage, um beipsielsweise testübergreifende Daten abzulegen.
Welche Implementation des StorageInterface dabei verwendet wird, interessiert mich in meinem Testfall selbst nicht.
Im Vorfeld möchte ich mit einem Guice-Binding definieren, wie das Interface automatisch durch Guice aufgelöst werden soll.

StorageInterface und Implementation

public interface StorageInterface {

    String store(Object obj);

    Object get(String uuid);
}


public class HashMapStorage implements StorageInterface {

    private final HashMap<String, Object> storage = new HashMap<>();

    @Override
    public String store(Object obj) {
        final UUID uuid = UUID.randomUUID();
        storage.put(uuid.toString(), obj);
        return uuid.toString();
    }

    @Override
    public Object get(final String uuid) {
        return storage.get(uuid);
    }
}

Guice Module erstellen

Nachdem nun klar ist, welche Klasse ich gern mittels Guice auflösen und injecten möchte, muss das Ganze in einem Guice-Modul definiert werden.

public final class JunitModule extends AbstractModule {

    protected void configure() {
        bind(StorageInterface.class).to(HashMapStorage.class).in(Scopes.SINGLETON);
    }
}

Als nächstes muss ein entsprechender GuiceInjector definiert werden. Dieser soll die entsprechenden Abhängigkeiten in den Tests auflösen. Dazu muss im Kontext von JUnit das Interace BeforeTestExecutionCallback und dessen Methode beforeTestExecution implementiert werden.
Hier ist die Zeile injector.injectMembers(o) die entscheidene Handlung. Falls im ExtensionContext von eine TestInstance vorhanden ist, so werden alle Guice-Member dieses Objekts automatisch aufgelöst.

public class GuiceInjector implements BeforeTestExecutionCallback {

    protected Injector injector = Guice.createInjector(new JunitModule());

    @Override
    public void beforeTestExecution(ExtensionContext context) throws Exception {

        final Optional<Object> testInstance = context.getTestInstance();
        testInstance.ifPresent(o -> {
            injector.injectMembers(o);
            context.getStore(ExtensionContext.Namespace.create(getClass())).put(injector.getClass(), injector);
        });
    }
}

Um das Ganze in der Handhabung der Guice-Annotation von TestNG anzupassen, habe ich noch eine WithGuice-Annotation erstellt, welche dann den Testklassen hinzugefügt werden muss.


@ExtendWith(GuiceInjector.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD})
public @interface WithGuice {

    Class<? extends Module>[] modules() default {};
}

Tests

Die Verwendung der Annotation und von Guice in den Testklassen selbst kann dann ganz nachdem Vorbild und Vorgehen von TestNG passieren. Wichtig ist, entsprechende Member des Tests mit der Inject-Annotation und die Testklasse oder eine abstrakte Testklasse mit der WithGuice-Annotation zu versehen.


@WithGuice(modules = JunitModule.class)
public class StorageTest {

    @Inject
    StorageInterface storage;

    @Test
    public void testT01_SimplePassedTestCase() {

        Assertions.assertNotNull(storage, "Guice modules injected successfully");

        final String stringIdent = storage.store("Simple String");
        final String integerIdent = storage.store(10);
        Assertions.assertEquals(storage.get(stringIdent), "Simple String");
        Assertions.assertEquals(storage.get(integerIdent), 10);
    }
}

Links

Internal:

External:

Zurück