Wiki > Grails-Kochbuch > Übergabe von Daten an WebTest
Problem
Es sollen Systemtests
mit WebTest
erstellt werden, wobei die zu testenden Datenkonstellationen nicht (effizient) über die Anwendung ermittelt werden können (z.B. mittels einer Suche).
Als erschwerende Hürde ist keine Testdatenbank verfügbar/zu nutzen, sondern es steht ausschließlich eine Echtdatenbank bereit.
Wichtiger Hinweis:
Wenn möglich sollte immer eine Möglichkeit gesucht werden, mit statischen/generierten Testdaten zu arbeiten. Dies sorgt für wesentlich stabilere Tests -- und es ist leichter, testgetrieben zu entwickeln
(engl.: test driven development, TDD).
Beispielszenario
Als Beispiel soll hier eine Personendatenbank dienen, bei der neben den reinen Personendaten auch die Beziehungen zu anderen Personen notiert sein sollen.Der Anwendungsfall enthält den folgenden Passus:
1) Steht die Person zu keiner anderen in Beziehung, so erscheint ein entsprechender textueller Hinweis.
2) Steht die Person zu genau einer anderen Person (B) in Beziehung, so sind die Personendaten von B rechts neben denen von A anzuzeigen.
3) Steht die Person mit mehreren Personen C1...Cn in Beziehung, so ist eine Liste der Namen C rechts von den Personendaten von A anzuzeigen.
Die zu erstellenden Tests für 1, 2 und 3 sind somit direkt davon abhängig, entsprechende Datenkonstellationen in der Datenbank zu kennen.
Kann der Test gegen eine Testdatenbank erfolgen, deren Daten z.B. in der Klasse BootStrap aufgebaut werden, so ist es trivial möglich, entsprechende IDs zu vergeben und den WebTest dementsprechend aufzubauen.
Leider gibt es jedoch gelegentlich den Fall, daß der Test zwingend gegen die Echtdatenbank ausgeführt werden muss und diese viele (mehrere Mio.) Datensätze enthalten kann. Damit entfällt die Möglichkeit, alle Datensätze iterativ abzuarbeiten.
Lösungen
Bitte beachten:
Beide Varianten senken die Wartbarkeit und führen möglicherweise zu instabilen Tests.
Variante A
Ein Ansatz ist der, einen Controller zu schreiben, der die entsprechenden Daten liefert. Die URL darf dann nicht öffentlich genutzt und muss zusätzlich noch mit Berechtigungen versehen werden.
Dies birgt jedoch grundsätzlich die Gefahr für Sicherheitslöcher.
Variante B
Ein anderer Ansatz ist, die benötigten Daten zu einem vorherigen Zeitpunkt -- im Rahmen der Integrationstest -- zu ermitteln und geeignet an den Systemtest zu übergeben. Dieser Ansatz wird im Folgenden beschrieben.
Zunächst wird ein Integrationstest benötigt, der die Testfälle aufsammelt und die IDs der Personentitäten in eine Property-Datei schreibt.
class EntityFinderTests
extends GroovyTestCase {
void testFindeEntitaeten() {
File file = new File("entity.properties")
if (file.exists()) {
file.delete()
}
file << "# automatisch generierte Datei\n\n"
// Person-Id -- ohne Beziehung
Person person = findePersonOhneBeziehung()
file << "person.ohne=${person?person.id:-1}\n"
... u.s.w. ...
}
}
Die Programmlogik der Klasse EntityFinderTests findet sich auch in Lösungsvariante A wieder, ist dort aber als "Fachlogik" getarnt, die sie nicht ist.
Werden z.B. die Tests genutzt um eine neue -- aber äquivalente -- Anwendung zu erstellen, wird der Entwickler gezwungen auch eben diese "Fachlogik" nachzubauen.
Die Funktionsweise der Methode findePersonOhneBeziehung() wird hier nicht näher erläutert. Der Name sollte jedoch hinreichend genau wiedergeben, daß mit einer geeigneten Datenbankanfrage eine Person ohne Beziehung ermittelt wird.
Für die anderen Fälle (exakt eine Beziehung und mehr als eine Beziehung) werden analoge Aufrufe benötigt.
Damit existiert nun im Hauptverzeichnis des Grails-Projektes die Datei entity.properties:
# automatisch generierte Datei person.ohne=42 person.eine=4711 person.mehrere=-1
Jetzt wird eine WebTest-Klasse erzeugt, die über eine kleine Hilfsmethode getId(propname) verfügt:
class PersonTest
extends grails.util.WebTest {
private Properties defaultProps = null
private final static String MUTEX = "PersonTestMutex"
/** Liefert die ID aus dem Property-Namen */
protected long getId(String propname) {
synchronized(MUTEX) {
if (defaultProps == null) {
try {
Properties props = new Properties()
defaultProps.load(new FileInputStream("entity.properties"))
defaultProps = props // <-- Wichtig, damit wir auf Mehrkernprozessoren keine Probleme im Multi-Threading bekommen. ;)
println "entity.properties wurde erfolgreich gelesen"
}
catch (IOException ausnahme) {
ausnahme.printStackTrace()
}
}
}
return Long.parseLong(defaultProps.getProperty(propname));
}
def testPersonOhneBeziehung() {
if (getId('person.ohne')==-1) {
throw new IllegalStateException("Es liegt keine Person ohne Beziehung im Datenbestand vor! Daher wird der Testfall übersprungen.")
}
webtest('Person ohne Beziehung'){
invoke(url: "person/view/${getId('person.ohne')}")
... eigentlicher Test ...
}
}
}
Damit kann der Test auf den zuvor dynamisch ermittelten Datensätzen aufbauen.
Im "regulären" Projektalltag sollten nun die Integrationstests
vor den Systemtests
ausgeführt werden, um sicherzustellen, daß die IDs weiterhin korrekt sind.
Diskussion
Dafür werden in Variante B die Tests voneinander abhängig, was TDD und Wartbarkeit erschwert. -- Stefan RoockIn beiden Varianten sind die Tests von den Methoden zum Auffinden benötigter Entitäten abhängig. In Variante A ist die Klasse EntityFinderTests lediglich als "Fachlogik getarnt" (als Controller). Ergo sind beide Varianten dbzgl. gleich gut oder gleich schlecht.
Lediglich in punkto Sicherheit hat Variante B einen kleinen Vorteil.
Zusätzlich finde ich, daß in die Anwendung nichts reingehört, was ausschließlich zum Testen genutzt wird. Was wiederum ein sauberes Design bedeutet. ;-)
Die Wartbarkeit leidet in beiden Fällen gleich stark. Aber dies liegt an der Problemstellung, daß eben keine statischen Testdaten genutzt werden können.
Es gibt eben nicht immer den Idealfall, daß sich alles testgetrieben entwickeln lässt. ;-)
Ich habe den Artikel bzgl. unserer hier aufgeführten Gedanken ergänzt.
-- Marc Pompl