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.