Im täglichen Projektgeschäft kommt es noch immer vor, dass Entwickler und auch Projektleiter vom Nutzen von Tests überzeugt werden müssen. Um in einem Umfeld, in dem bis auf Tests durch die Qualitätssicherung noch keine oder nur Alibi-Tests vorliegen, ist es ratsam, nicht gleich den Big Bang zu starten und TDD (Test Driven Development) einführen zu wollen. Ratsam ist eine Orientierung an der klassischen Testpyramide, bei welcher Unit-Tests die Voraussetzung für weitere Tests darstellen.
Sind die Entwickler und Projektleiter einmal ins Boot geholt, muss eine zweite Hürde überwunden werden. Es muss den Entwicklern eine konfigurierte Umgebung zur Verfügung gestellt werden, in welcher diese ihre Tests ohne großen Aufwand schreiben und ausführen können. Welche Umgebung vonnöten ist, um im Spring 2.5 Context mit JUnit 4.4 und dem Mock-Framwork Mockito in Version 1.8.5 testen zu können, soll dieser Blogbeitrag klären.
Einer der großen Vorteile des Einsatzes von Spring ist die Dependency Injection (DI), welche ja jetzt auch aktuell für Java Webanwendungen mit CDI und der entsprechenden Referenzimplementierung Weld von JBoss Einzug in die Java 6 Spezifikation erhält.
Es müssen also in einem Unit-Test die von einem zu testenden Service genutzten Klassen im Context vorhanden sein, um diese auch im Test nutzen bzw. injizieren zu können. Um einen entsprechenden Context im Test zur Verfügung zu stellen, bietet das Test-Package von Spring die Annotation @org.springframework.test.context.ContextConfiguration
, welche erlaubt, einen Context zu laden. Um wiederum JUnit mitzuteilen, dass mit Spring getestet wird, muss ein entsprechender Classrunner mit der Annotation @org.junit.runner.RunWith
definiert werden, welcher diesen Context für den Test bereitstellt.
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { '/application-config.xml' }) public class FrontendServiceTest { // Zu testender FrontendService @Resource private FrontendService frontendService; … }
Dank dieser zwei Annotationen können die Unit-Tests jetzt im Spring Context ausgeführt, der zu testende FrontendService in den Test injiziert und die im FrontendService genutzten Services in diesen injiziert werden.
@Service public class FrontendService { // injizierter entferner BackendService @Resource protected BackendService backendService; public String getBackendName() { return backendService.getName(); } }
In der aktuellen Konfiguration wird jedoch auf die „echten“ in der application-config.xml
konfigurierten Services zugegriffen. Es würde sich folglich schon eher um einen Integrationstest handeln. Hier soll jedoch ein Unit-Test vollkommen unabhängig von Backend-Daten durchgeführt werden. Dazu soll anstatt mit den „echten“ Services mit Mock-Objekten gearbeitet werden.
Um im Spring Context Mock-Objekte definieren zu können, die dann anstatt der „echten“ Services injiziert werden, wird das Spring-Interface der Factory Bean genutzt und mit einer MockFactory implementiert.
public class MockFactory implements FactoryBean { // Typ des erstellten Objektes private Class type; public void setType(final Class type) { this.type = type; } @Override public Object getObject() throws Exception { // wird das Objekt angefordert, wird ein entsprechendes Mock-Objekt // erzeugt und zurückgegeben return mock(type); } @Override public Class getObjectType() { return type; } @Override public boolean isSingleton() { return true; } }
Anstatt der application-config.xml
wird im Test eine für den Test angepasste test-application-config.xml
geladen. Anstatt der Konfiguration für den „echten“ Service wird der Service über die MockFactory als Mock-Objekt konfiguriert.
<!-- Konfigurieren des BackendServices als Mock --> <bean id='contractServiceDelegate' class='de.holisticon.blog.springContextTest.MockFactory'> <property name='type' value='de.holisticon.blog.enterprise.springContextTest.BackendService' > </bean>
Der eigentliche Test ist jetzt ganz einfach. Ratsam ist, das Verhalten der verwendeten Mockobjekte in einer mit @org.junit.Before
annotierten Methode zu definieren und die Mockobjekte nach dem Test innerhalb einer mit @org.junit.After
annotierten Methode aufzuräumen, damit keine Wechselwirkungen beim Ausführen mehrerer Tests innerhalb eines Contexts entstehen.
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { '/test-application-config.xml' }) public class FrontendServiceTest { // Zu testender FrontendService @Resource private FrontendService frontendService; private final String BE_NAME = 'test'; /** * Bevor der Test ausgeführt wird, wird das gewünschte Verhalten des Mocks * definiert. * * @throws Exception */ @Before public void init() throws Exception { when(frontendService.backendService.getName()).thenReturn(BE_NAME); } /** * Test der Methode getBackendName() des FrontendServices. * * @throws Exception */ @Test public void getBackendNameTest() throws Exception { String resultString = frontendService.getBackendName(); assertNotNull(resultString); assertEquals(BE_NAME, resultString); } /** * Nach Ausführung des Test wird der Mock für den BackendService * zurückgesetzt, damit keine Wechselwirkungen mit anderen Tests entstehen * können. * * @throws Exception */ @After public void clean() throws Exception { reset(frontendService.backendService); } }
Der Quellode ist in einer kleinen Beispielapplikation zu finden: SpringContextTest
Wenn Spring im Projekt bereits als DI Framework gesetzt ist, ist das sicher ein guter Ansatz. Für den Fall, dass die Entscheidung ansteht, extra für das Testing ein DI Framework einzuführen, empfehle ich einen Blick auf Google Guice (https://www.holisticon.de/2011/01/junit-mit-guice-test-runnern/). Guice ist leichtgewichtig und „single jar“ ohne XML Konfiguration und daher u.U. leichter in die bestehende Architektur zu integrieren.