Unit Tests mit Spring und asynchronem Verhalten

Wieder einmal ein Anwendungsfall aus dem aktuellen (privaten) Projekt. Wenn ein Benutzer seine Bestellung abschliesst, soll diese gespeichert, eine PDF-Rechnung generiert und eine Email mit Attachment versendet werden. Ich habe beschlossen, nach dem Speichern der Bestellung via Hibernate, dass Erstellen der Rechnung und den Email Versand in einen separaten Thread auszulagern, der asynchron ausgeführt wird. Dadurch kehrt die Webanwendung schneller wieder zum Benutzer zurück. PDF Erstellung und Email Versand dauern nämlich 3 bis 5 Sekunden. Unter Useability Gesichtspunkten also durchaus positiv.

Das Starten des Threads ist in eine Klasse OrderProcessor ausgelagert.



package com.mini.biz.order;

import com.mini.biz.tools.mail.EmailProcessor;
import com.mini.biz.fop.pdf.PdfGenerator;
import com.mini.biz.entities.cart.Cart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;

/**
* The {@link OrderProcessor} handles further processing once an
* order has been saved in the system.
*/
public class OrderProcessor
{
@Autowired
private TaskExecutor _taskExecutor;

@Autowired
private PdfGenerator _pdfGenerator;

@Autowired
private EmailProcessor _emailProcessor;


public void processOrder(final Cart cart)
{
Runnable runnable = new Runnable()
{
public void run()
{
m_pdfGenerator.generate(cart);
m_emailProcessor.sentEmailForNewOrder(cart);
}
};
_taskExecutor.execute(runnable);
}
}



Auf die Details vom PdfGenerator und EmailProcessor gehe ich an dieser Stelle nicht weiter ein. Wichtig ist der TaskExecutor. Er ist wie folgt konfiguriert.



<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5" />
<property name="maxPoolSize" value="10" />
<property name="queueCapacity" value="25" />
</bean>



Leider nimmt der TaskExecutor nur Runnable Objekte entgegen. Ich persönlich finde das mit Java5 eingeführte Callable Interface schöner – aber gut.

Mit dem Spring Framework und TestNG lässt sich das alles wunderbar testen. Das eingesetzte Web Framework Apache Wicket lassen wir mal ausser Acht.



@Test
@ContextConfiguration(locations={"/applicationContext.xml"})
public class ApproveAndOrderPanelTest extends AbstractTestNGSpringContextTests
{



Hier wird im Spring 2.5 Stil der ApplicationContext referenziert. TestNG Tests müssen leider (im Gegensatz zu JUnit 4.x Tests) immer noch von AbstractTestNGSpringContextTests ableiten. Übrigens sollte man AbstractTestNGSpringContextTests nicht vor Spring 2.5.2 einsetzen, da dort ein wichtiger Bug behoben wurde, der auftritt wenn man die komplette TestNG Suite laufen lässt (AbstractTestNGSpringContextTests executes Spring default callbacks in any case (marked with "alwaysRun = true"). Die @Test Annotation gehört zu TestNG. Kommen wir zum Code der vor den Tests ausgeführt wird:



@BeforeClass
public void before()
{
// Not mentioned here: delete existing PDF File

// Start Wiser SMTP
_wiser = new Wiser();
_wiser.setPort(2525);
_wiser.start();

// Alter MailSenderBean
JavaMailSenderImpl mailSender = (JavaMailSenderImpl) _mailSender;
mailSender.setPort(2525);
mailSender.setHost("localhost");
}
}



Hier starte ich einen Dummy SMTP Server von den Subversion Jungs. Anschliessend „verbiege“ ich die MailSender Bean damit der Dummy SMTP Server verwendet wird. Achtung, wenn Ihr weitere Unit Tests innerhalb der Suite laufen lasst, denkt dran dass Spring Beans in der Regel Singletons sind. Das „Verbiegen“ einer Bean schlägt also auch auf Folgetests durch, wenn es nicht in einer tearDown Methode wieder rückgängig gemacht wird. Nun der eigentliche (start gekürzte) Test.



public void testPanel() throws InterruptedException
{
// not shown: set up mock Bestellung aka Cart

assertEquals(0, _wiser.getMessages().size());
File pdfFile = _pdfGenerator.getPdfFile(cart);
assertFalse(pdfFile.exists());

// not shown: use WicketTester and FormTester to submit HTML Form and trigger order processing

ThreadPoolTaskExecutor threadPoolTaskExecutor = (ThreadPoolTaskExecutor) _taskExecutor;
int activeThreads = threadPoolTaskExecutor.getActiveCount();
while (activeThreads > 0)
{
TimeUnit.SECONDS.sleep(3);
activeThreads = threadPoolTaskExecutor.getActiveCount();
}

pdfFile = _pdfGenerator.getPdfFile(cart);
assertTrue(pdfFile.exists());

assertEquals(1, _wiser.getMessages().size());
// not shown: check that email contains attachment
}



Zunächst überprüfe ich, dass keine PDF Datei existiert und keine Email in der Queue vorhanden ist. Dann wir, hier nicht aufgeführter Code ausgeführt um den OrderProcessor anzuwerfen, so dass dieser einen neuen Thread spawned. Der Thread läuft im Spring konfigurierten TaskExecutor, welcher in Wirklichkeit ein ThreadPoolTaskExecutor ist. Da der TestNG Test weiterläuft, muss ich warten bis alle Threads (in meinem Fall also genau ein einziger Thread) im ThreadPoolTaskExecutor beendet sind. Anschliessend überprüfe ich, ob ein PDF erzeugt und eine Email versendet wurde.

Bleibt nur noch das Aufräumen.



@AfterClass
public void after()
{
_wiser.stop();
}



So richtig mächtig ist das Ganze eigentlich erst unter dem Gesichtspunkt, dass es sich dank WicketTester und FormTester eigentlich gar nicht mehr um einen Unit Test, sondern um einen GUI-Test (Integrationstest) handelt – auch wenn dieser Code hier nicht aufgeführt ist. Apache Wicket wird mit Testklassen ausgeliefert, die Selenium oder Watir (von Layout Aspekten abgesehen) überflüssig machen.