Integrationstests mit Spring

14 views
Skip to first unread message

Sascha Schäfer

unread,
Aug 27, 2009, 7:19:41 AM8/27/09
to Spring User Group Germany
Hallo,

ich bin dabei, für meine DAO-Objekte Integrationstests zu schreiben.
Dazu nutze ich Spring. All meine Testklasse erben von der Spring-
Klasse "AbstractTransactionalDataSourceSpringContextTests". Das hat ja
den Vorteil, dass all mein Änderungen an den Daten in der Datenbank
nach dem Test wieder rückgängig gemacht werden. Echt ne tolle
Sache ... dachte ich anfangs.
Nach einigen erfolgreichen Tests tauchen nun die ersten Probleme auf.
Ich möchte nämlich jetzt die save-Methode eines DAOs testen. Im
folgenden liste ich einen Teil der Testmethode auf:

int buildingCountBefore = jdbcTemplate.queryForInt( "SELECT count(*)
FROM building" );

// Daten einfügen, die nach dem Beenden der Methode automatisch
gelöscht werden
String buildingId = "Unittest";
Building building = new Building();
building.setId( buildingId );
building.setName( "Testgebäude-Unittest" );
final BuildingDAO buildingDAO = ( BuildingDAO )
applicationContext.getBean( "buildingDAO" );
buildingDAO.saveBuilding( building );

int buildingCountAfter = jdbcTemplate.queryForInt( "SELECT count(*)
FROM building" );
assertEquals( "Die Anzahl der Gebäude wurde nicht um eins erhöht!",
buildingCountBefore + 1, buildingCountAfter );

Wie man sieht, handelt es sich hier um eine Tabelle von Gebäuden. Der
dargestellte Teil des Tests schlägt fehl. Die Anzahl der Gebäude ist
vor und nach dem Speichern des Gebäudes gleich, obwohl Sie um eins
erhöht sein müsste. Laut dem Buch "Spring In Action" sollte mein Test
erfolgreich sein.
Dass meine save-Methode auch speichert habe ich daran erkannt, als ich
setComplete() in den gezeigten Codeabschnitt des Tests eingefügt habe
und anschließend in Blick in die Datenbank warf. Und da sah ich das
Gebäude, was vorher nicht da war.
Gibt es hier irgendein Problem mit dem Caching? Ich habe allerdings
schon das Hibernate-Caching abgeschhaltet, glaube ich zumindest.

Liebe Grüße
Sascha

Philip May

unread,
Aug 27, 2009, 7:34:42 AM8/27/09
to su...@googlegroups.com
Das:

final BuildingDAO buildingDAO = ( BuildingDAO )
applicationContext.getBean( "buildingDAO" );

Sollte eigendlich nicht nötig sein.

Du musst die nicht aktiv die Bean holen, sondern auch hier kann der DI Mechanismus von Spring genutzt werden.

Definiere buildingDAO als Klassen-Attribut und schreib einen Setter dafür. Der Rest wird automatisch gesetzt.

Vielleicht hängt es damit zusammen?

Schöne Grüße
Philip May


2009/8/27 Sascha Schäfer <ders...@t-online.de>

Sascha Schäfer

unread,
Aug 27, 2009, 8:12:55 AM8/27/09
to Spring User Group Germany


On 27 Aug., 13:34, Philip May <eniak.i...@googlemail.com> wrote:
> Du musst die nicht aktiv die Bean holen, sondern auch hier kann der DI
> Mechanismus von Spring genutzt werden.
>
> Definiere buildingDAO als Klassen-Attribut und schreib einen Setter dafür.
> Der Rest wird automatisch gesetzt.
>
> Vielleicht hängt es damit zusammen?
>
> Schöne Grüße
> Philip May
>

Vielleicht sollte ich noch etwas zu meinem gezeigten Codeschnipsel
sagen. Der Codeausschnitt ist Teil einer Testmethode, und die Methode
ist Teil einer Testklasse. Die Testklasse definiere ich nicht im
Spring-Kontext, was meines Erachtens richtig ist. Deshalb kann ich in
einer Testklasse auch nicht die DI von Spring nutzen.
Das Speichern funktioniert ja auch, nur das jdbcTemplate bekommt es
nicht mit, obwohl es das sollte.

Liebe Grüße
Sascha

Philip May

unread,
Aug 27, 2009, 8:19:29 AM8/27/09
to su...@googlegroups.com
Soviel ich weiss baut die Basisklasse den Spring Container selber auf und verdratet den JUnit Test per Auto Wiring selber.

Klingt etwas magisch ... ist es aber auch...

Schöne Grüße
Philip May


2009/8/27 Sascha Schäfer <ders...@t-online.de>



Sascha Schäfer

unread,
Aug 27, 2009, 8:39:53 AM8/27/09
to Spring User Group Germany
On 27 Aug., 14:19, Philip May <eniak.i...@googlemail.com> wrote:
> Soviel ich weiss baut die Basisklasse den Spring Container selber auf und
> verdratet den JUnit Test per Auto Wiring selber.
>
> Klingt etwas magisch ... ist es aber auch...
>
> Schöne Grüße
> Philip May

Könnte man so sagen. Meine Testklasse erbt ja von der "Spring-
Basisklasse" und erzeugt damit den Spring-Container selbst. Deshalb
kann ich ja auch nicht die DI von Spring in meiner Testklasse nutzen.

Liebe Grüße
Sascha

Sam Brannen

unread,
Aug 28, 2009, 6:23:52 AM8/28/09
to Spring User Group Germany
Hi Sascha,

> Meine Testklasse erbt ja von der "Spring-
> Basisklasse" und erzeugt damit den Spring-Container selbst. Deshalb
> kann ich ja auch nicht die DI von Spring in meiner Testklasse nutzen.

Das stimmt leider nicht. Philip hatte Recht: "Du musst nicht aktiv die
Bean holen, sondern auch hier kann der DI Mechanismus von Spring
genutzt werden."

Darueber hinaus, der DI Mechanismus von Spring sollte genau an dieser
Stelle genutzt werden. Das ist eigentlich einer der Hauptziele von den
Spring-Basisklassen!

Deine Klasse leidet von
AbstractTransactionalDataSourceSpringContextTests ab, welche uebrigens
eine Subklasse von AbstractDependencyInjectionSpringContextTests ist.
Direkt aus dem JavaDoc von
AbstractDependencyInjectionSpringContextTests:

Convenient superclass for JUnit 3.8 based tests depending on a Spring
context. The test instance itself is populated by Dependency
Injection.

This supports two modes of populating the test:
- Via Setter Dependency Injection. Simply express dependencies on
objects in the test fixture, and they will be satisfied by autowiring
by type.
- Via Field Injection. Declare protected variables of the required
type which match named beans in the context. This is autowire by name,
rather than type. This approach is based on an approach originated by
Ara Abrahmian. Setter Dependency Injection is the default: set the
populateProtectedVariables property to true in the constructor to
switch on Field Injection.

Uebrigens, welche Version von Spring verwendest du? Wenn du schon auf
2.5.x bist, dann wuerde ich auf jeden Fall empfehlen, das Spring
TestContext Framework zu verwenden, statt die "legacy" JUnit 3.8
Basisklassen. Die alten JUnit 3.8 Basisklassen sind ab Spring 3.0
sowieso "deprecated".

Bezueglich deines Problems, hast du versucht, die Hibernate Session zu
flushen, statt setComplete() aufzurufen? Beim Save muss man
normalerweise nicht flushen, aber ich muesste dein komplett Setup
sehen, um das besser zu analysieren. In HibernateSessionFlushingTests
(Link unten) kannst du ein passendes Beispiel mit Session-Flushing
anschauen. Uebrigens, die assertPersonCount()-Methode habe ich gerade
fuer Dich in der saveJuergenWithDriversLicense()-Methode eingebaut, um
dir zu zeigen, dass es tatsaechlich geht. ;)

https://fisheye.springsource.org/browse/spring-framework/trunk/org.springframework.test/src/test/java/org/springframework/test/context/junit4/orm/HibernateSessionFlushingTests.java?r=HEAD

Viele Erfolg!

Sam

Sascha Schäfer

unread,
Aug 28, 2009, 8:19:07 AM8/28/09
to Spring User Group Germany
Hallo Sam,

erstmal vielen Dank für deine ausführliche Antwort. Auch vielen Dank
an Philip.

Ich habe den Inhalt deines Links studiert. Dabei habe ich bemerkt,
dass ich bereits das "TestContext"-Framework von Spring nutze.

Ich habe meinen Test nach dem Muster umgestellt, der hinter deinem
Link beschrieben wird. Der Test läuft nun durch. Speichern und Löschen
wurden erfolgreich durchgeführt.

Aber ich verstehe aber ein paar Sachen noch nicht. Ich bin wohl ein
noch nicht sehr erfahrener Spring-Anwender.
Mein gezeigter Test ist ein Integrationstest. Ich teste gegen eine
konkrete Datenbank. Laut dem Buch "Spring In Action" ist der sicherste
Weg für Integrationstests, die Methoden aus der Service-Klasse mit
JDBC-Abfragen zu prüfen. Man sollte also nicht für die Überprüfung
erneut Methoden aus der Service-Klasse verwenden. Klingt für mich
logisch.
Die Spring-Klasse
"AbstractTransactionalDataSourceSpringContextTests" (von der ich erbe)
unterstützt das Abfragen per JDBC, da diese Klasse bereits ein JDBC-
Template Objekt mitbringt. Wenn Spring 3.0 diese Spring-Klasse als
deprecated markiert, wie führt man denn dann Integrationstests in
Spring 3.0 durch? Muss man sich hier selbst ein JDBC-Template im
Spring-Kontext erzeugen?

Liebe Grüße
Sascha

Sam Brannen

unread,
Aug 28, 2009, 9:35:45 AM8/28/09
to Spring User Group Germany
Hi Sascha,

> Ich habe den Inhalt deines Links studiert. Dabei habe ich bemerkt,
> dass ich bereits das "TestContext"-Framework von Spring nutze.

Wenn du "AbstractTransactionalDataSourceSpringContextTests"
verwendest, dann verwendest du nicht das TestContext-Framwork sondern
die bald "deprecated"/legacy JUnit 3.8 Basisklassen.

Beim TestContext-Framework: entweder solltest du deine Test-Klasse mit
@RunWith(SpringJUnit4ClassRunner.class) annotieren, oder deine Test-
Klasse sollte von einer Klasse des TestContext-Frameworks ableiten.

Schau mal hier: http://static.springsource.org/spring/docs/2.5.x/reference/testing.html#testcontext-support-classes

> Ich habe meinen Test nach dem Muster umgestellt, der hinter deinem
> Link beschrieben wird. Der Test läuft nun durch. Speichern und Löschen
> wurden erfolgreich durchgeführt.

Super! :)

> Aber ich verstehe aber ein paar Sachen noch nicht. Ich bin wohl ein
> noch nicht sehr erfahrener Spring-Anwender.

Kein Problem. Wir wissen, dass es beim Testing ein bisschen verwirrend
sein kann, besonders weil es derzeit noch 2 Testing Frameworks von
Spring gibt. ;)

Deshalb haben wir die alten JUnit 3.8 Basisklassen als deprecated
markiert; die werden auch nicht mehr unterstuetzt oder erweitert.

> Mein gezeigter Test ist ein Integrationstest. Ich teste gegen eine
> konkrete Datenbank. Laut dem Buch "Spring In Action" ist der sicherste
> Weg für Integrationstests, die Methoden aus der Service-Klasse mit
> JDBC-Abfragen zu prüfen. Man sollte also nicht für die Überprüfung
> erneut Methoden aus der Service-Klasse verwenden. Klingt für mich
> logisch.

Das stimmt: per JDBC kommst du direkt auf die Datenbank. Allerdings
muss man manchmal die Session flushen, wenn man mit einem O/R Mapper
(e.g., Hibernate oder JPA) arbeitet.

> Die Spring-Klasse
> "AbstractTransactionalDataSourceSpringContextTests" (von der ich erbe)
> unterstützt das Abfragen per JDBC, da diese Klasse bereits ein JDBC-
> Template Objekt mitbringt. Wenn Spring 3.0 diese Spring-Klasse als
> deprecated markiert, wie führt man denn dann Integrationstests in
> Spring 3.0 durch? Muss man sich hier selbst ein JDBC-Template im
> Spring-Kontext erzeugen?

Nein, das Spring TestContext Framework unterstuetzt das auch. Am
einfachsten kannst du von
AbstractTransactionalJUnit4SpringContextTests ableiten. Das ist aber
kein Muss; du kannst auch deine Test-Klasse direkt annotieren, wie
folgt:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@Transactional
public class MyIntegrationTest {

protected SimpleJdbcTemplate simpleJdbcTemplate;

@Autowired
public void setDataSource(DataSource dataSource) {
this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource);
}

// ...
}

Schau mal hier fuer Details:
http://static.springsource.org/spring/docs/2.5.x/reference/testing.html#testcontext-tx

Gruss,

Sam


p.s. zur Info: SimpleJdbcTemplate ist quasi der Nachfolger von
JdbcTemplate.

Sascha Schäfer

unread,
Aug 31, 2009, 3:10:16 AM8/31/09
to Spring User Group Germany
Ok, jetzt habe ich es verstanden, glaube ich. Miene Tests laufen nun
auch mit gleichezeitiger Überprüfung per JDBC. Dabei habe ich bemerkt,
dass die Klasse "AbstractTransactionalJUnit4SpringContextTests"
bereits ein Objekt vom Typ SimpleJdbcTemplate mitliefert. Dieses
Objekt hab ich nicht bemerkt, da ich nach einem vererbarem Attribut
names jdbcTemplate gesucht habe. So hieß es nämlich in dem "alten"
Framework.

Also alles super jetzt. Vielen Dank an Alle.

Liebe Grüße
Sascha
Message has been deleted

Sascha Schäfer

unread,
Sep 2, 2009, 11:10:01 AM9/2/09
to Spring User Group Germany
Hallo,

ich habe mich leider geirrt. Der Test läuft doch nicht durch. Wenn ich
per JDBC die Anzahl der vorhandenen Datensätze abfrage, ist diese vor
und nach dem Speichern identisch.

Ich habe in Bezug auf mein ersten Test folgendes geändert. Meine
Klasse erbt nun von AbstractTransactionalJUnit4SpringContextTests.
Meine SQL-Statement führe ich nun nicht mehr mit jdbcTemplate aus,
sondern mit simpleJdbcTemplate. Ich zeige im Folgenden meine
Testklasse:

@ContextConfiguration(locations = { "/spring/
dataAccessTestContext.xml" })
public class NewBuildingDAOTest extends
AbstractTransactionalJUnit4SpringContextTests
{
@Autowired
protected BuildingDAO buildingDAO;

@Test
@Rollback(true)
public void testSaveBuilding() throws AfmDataAccessException
{
final int buildingCountBefore = simpleJdbcTemplate.queryForInt
( "SELECT count(*) FROM bl" );

// Daten einfügen, die nach dem Beenden der Methode automatisch
gelöscht werden
final String buildingId = "Unittest";
final Building building = new Building();
building.setId( buildingId );
building.setName( "Testgebäude-Unittest" );
buildingDAO.saveAfmObject( building );

final int buildingCountAfter = simpleJdbcTemplate.queryForInt
( "SELECT count(*) FROM bl" );
assertEquals( "Die Anzahl der gebäude wurde nicht um eins erhöht!",
buildingCountBefore + 1, buildingCountAfter );

final Building buildingLoaded = buildingDAO.getBuildingById
( buildingId );
assertNotNull( "Das gespeicherte Gebäude wurde nicht gefunden!",
buildingLoaded );
assertEquals( "Die ID stimmt nicht überein!", building.getId(),
buildingLoaded.getId() );
}
}

Dieser Test läuft nicht durch, da die Anzahl der Datensätze vor und
nach dem Speichern gleich ist, obwohl sie um eins erhöht sein müsste.
Kann mir jemand eine Erklärung dafür geben?

Ist dieser Thread eigentlich noch aktuell oder soll ich einen neuen
eröffnen?

Liebe Grüße
Sascha

Sascha Schäfer

unread,
Sep 3, 2009, 3:59:43 AM9/3/09
to Spring User Group Germany
Hallo,

mit einem sessionFactory.getCurrentSession().flush() nach dem Aufrufen
der Save-Methode läuft der Test erfolgreich durch.

Liebe Grüße
Sascha

Sam Brannen

unread,
Sep 3, 2009, 5:56:03 AM9/3/09
to Spring User Group Germany
> mit einem sessionFactory.getCurrentSession().flush() nach dem Aufrufen
> der Save-Methode läuft der Test erfolgreich durch.

Schoen, dass du es mit dem Flush geschafft hast!

Uebrigens, simpleJdbcTemplate.queryForInt("SELECT count(*) FROM bl")
kannst du ganz einfach durch countRowsInTable("bl") ersetzen, weil
deine Klasse jetzt von AbstractTransactionalJUnit4SpringContextTests
ableitet. ;)

Gruss,

Sam
Reply all
Reply to author
Forward
0 new messages