Blog

JUnit mit Guice Test-Runnern

Das Dependency Injection Framework Google Guice kann eine große Hilfe beim Testen mit JUnit sein. Es kann als dynamische Factory sowohl Mock-Objekte als auch Alternativ-Implementierungen bereitstellen und so den Einrichtungsaufwand erheblich verringern.
Koppelt man dieses Konzept mit eigenen Test-Runnern, ist das Ziel einer Zero-Config Test-Suite zum Greifen nahe.

Betrachten wir dazu das folgende Beispiel-Szenario. TodayService ist eine konkrete Implementierung von DateService, die das aktuelle Datum liefert. Die Guice-Annotation @ImplementedBy definiert ihn dabei als Default-Implementierung.

// DateService
@ImplementedBy(TodayService.class)
public interface DateService {
	public Date getDate();
}
// TodayService
public class TodayService implements DateService {
	@Override
	public Date getDate() {
		return new Date();
	}
}

Unser DateServiceClient wird dieses Datum als „dd.mm.yyyy“ ausgeben. Den benötigten Service bekommt der Konstruktor injiziert, die Vorarbeiten übernimmt die Main-Methode.

public class DateServiceClient {
	private Date today;
	@Inject
	public DateServiceClient(final DateService dateService) {
		super();
		today = dateService.getDate();
	}
	@Override
	public String toString() {
		return DateFormat.getDateInstance(DateFormat.MEDIUM).format(today);
	}
}
// Runner
// ...
public static void main(String[] args) {
   Injector injector = Guice.createInjector();
   DateServiceClient client = injector.getInstance(DateServiceClient.class);
   System.out.println("Datum des Service: " + client.toString());
}
//...

Wenn wir diesen Code testen, benötigen wir einen DateService, der ein konstantes Datum ausgibt, damit wir eine Erwartung an den Client formulieren können. Wie in Geteilte Test-Fixtures mit JUnit vorgestellt, kann dazu ein eigener Runner implementiert werden, der die benötigten Daten bereitstellt.

Dies soll hier auch geschehen, allerdings unter Verwendung von Guice:

public abstract class GuiceTestRunner extends BlockJUnit4ClassRunner {
  public GuiceTestRunner(final Class<?> classToRun, Module... modules) throws InitializationError {
    super(classToRun);
    this.injector = Guice.createInjector(modules);
  }
  @Override
  public Object createTest() {
    return injector.getInstance(getTestClass().getJavaClass());
  }
//...
}

public class DateTestRunner extends GuiceTestRunner {
	public DateTestRunner(Class<?> classToRun) throws InitializationError {
		super(classToRun, new AbstractModule() {
			@Override
			protected void configure() {
				bind(DateService.class).toInstance(new DateService() {
					@Override
					public Date getDate() {
						return new Date(0L);
					}
				});}});
	}
}

Was ist passiert? Wir definieren einen Test-Runner, der analog zu unserer Main-Methode die Guice-Konfiguration beinhaltet, in diesem Fall, indem in einem Modul ein DateService implementiert wird, der immer den 1.1.1970 als Datum zurück liefert.

Das ermöglicht uns, den eigentlichen Test sehr einfach zu schreiben:

@RunWith(DateTestRunner.class)
public class DateServiceClientTest {
	private final DateServiceClient dateService;

	@Inject
	public DateServiceClientTest(final DateServiceClient dateService) {
		super();
		this.dateService = dateService;
	}

	@Test
	public void testToString() {
		assertEquals("01.01.1970", dateService.toString());
	}
}

Die Testkonfiguration wird so aus der setUp()-Methode in die Guice-Module des Testrunners verlagert. Aufwändiger „Boilerplate“-Code wird reduziert und Abhängigkeiten der Tests untereinander minimiert.

Holisticon AG — Teile diesen Artikel

Über den Autor

Jan Galinski ist Senior Consultant bei der Holisticon AG und seit vielen Jahren als Architekt und Entwickler in agilen Kundenprojekten unterwegs. Er ist ein leidenschaftlicher Prozessautomatisierer und BPM-Craftsman, am liebsten mit Camunda BPM. Als Contributor zu zahlreichen Open Source Projekten aus den Bereichen BPM und JEE gibt er seine Erfahrung und Wissen gerne weiter. 2016 wurde er mit dem Camunda Community Award ausgezeichnet.

Antwort hinterlassen