Wie erstellt ihr Testdaten für Integrationstests?

Hallo zusammen,

wollte mal fragen wie ihr Testdaten für Integrationstests erstellt?

Ich meine jetzt speziell Integrationstests die auf Service Ebene (bzw. DAO/Repository Ebene) arbeiten, die brauchen ja bestimmte Daten um zu funktionieren.

Generell fallen mir da ein paar Möglichkeiten ein, haben alle ihre Vor- und Nachteile:

  • Man nimmt die DAOs selber um Testdaten zu erzeugen.
    Vorteil: Man hat den Code schon.
    Nachteil: Die DAOs sind ja teil des zu testenden Systems, man ist naturgemäss eingeschränkt was die Verifikation betrifft. Mappingfehler können schnell übersehen werden bzw. sind schwer zu finden, weil zB. das falsche Mapping von einem Objekt Feld auf ein Tabellenfeld sowohl beim schreiben als auch beim lesen verwendet wird, d.h. man merkt diese Art Fehler gar nicht.
    Wenn man nun in bestimmten DAOs keine Methoden hat um zB. Daten anzulegen orer zu löschen weil die Fachlichkeit das nicht braucht, wirds problematisch, führt oft dazu dass diese Methoden, die nur von Tests genutzt werden, im Produktivcode enden.

  • Man nimmt ein Framework wie DBUnit, SetupDB, etc. pp. kann ausser Daten einfügen auch direkt in der DB prüfen ob Werte richtig angekommen sind bzw. richtig gelesen wurden.
    Vorteil: Man kann besser verifizieren, d.h. Mappingfehler sind einfach zu finden.
    Nachteil: Man muss den Code schreiben, inkl. der Mappinginformationen zwischen Objekten und Tabellen, hat also zumindest die Mappinginformationen und DB Schema informationen redundant, einmal in Prod Code, dann nochmal im Testcode.

  • SQL Files die einmal vor den Test eingespielt werden.
    Vorteil: Man bekommt halt Daten in die DB zum testen, können zB. auf einer DB ausgelesen werden die schon “richtige” Daten enthält.
    Nachteil: Die Mappingformationen sind auch hier redundant, wenn man die SQL file nicht manuell erstellt macht das nix. Allerdings ist es eher schwierig pro Testcase die sog. “minimale Fixture” einzuspielen, wird meist nur einmal für alle Test gemacht -> “Shared Fxiture”, d.h. Test werden wahrscheinlich interagieren (zB. ein Test verändert Daten der Shared Fixture um die Create/Update/Delete Methode des Services/DAOs zu testen, der nächste Test der die ursprüngliche Daten erwartet wird fehlschlagen wenn er nach den Test läuft die diese Daten verändern), oder die Test sind eher oberflächlich.

  • Tests erzeugen Daten die von anderen Test als Testfixture verwendet werden
    Nachteile: Abhängigkeiten zwischen Test, schlechte “defect localization”, nicht parallelisierbar, bei mehrmaligem Ausführen kann sich der Fehler “verändern”.

Persönlich bin ich ein Fan von DBUnit, trotz der Nachteile der zu schreibenden Codes und der redundanten Mapping Infos. Ersteres lohnt sich schnell wenn man die Builder verwendet (aufwändig zu schreiben, minimaler aufwand beim nutzen der Builder), letzteres lässt sich gut in den Griff bekommen wenn man dafür sorgt dass diese Mappinginformationen wirklich nur einmal Redundant gehalten werden.

Aber was mir wirklich wichtig ist, dass es Builder gibt die einem das erstellen der Testdaten für einen einzelnen Testcase so einfach macht, dass es keine Ausreden mehr gibt warum man keine sauberen Integrationstest schreibt (jeder Testcase erzeugt seine eigenen, einzigartigen Testdaten die in keinen anderen Test so vorkommen).
Factories für Testdaten (ObjetMother etc.) haben den Nachteil, dass die inflexibler sind als das Builder Pattern, funzen nur gut bei simplen Testdaten/Strukturen.

Die einzelnen Builder zu implementieren ist recht aufwändig, sie zu verwenden aber sehr einfach und kurz, man gibt nur noch Werte an die relevant für den Test sind, die anderen werden nicht von mir gesetzt sondern der Builder nimmt einfach Defaultwerte.

Wie macht ihr das so in euren Projekten?
Wie genau nimmt man es da mit Testen allgemein, Differenzierung zwischen Unittests und Integrationstest?
Vor allem: Wie bekommt ihr eure Testdaten in die DB??

Bei Unittests bin ich immer ganz klar für Mocken von sämtlichen Schnittstellen, damit auch für die DB.

Bei Integrationstests bin ich ein Fan von DB-Unit. In Zusammenarbeit mit http://jailer.sourceforge.net/ lassen sich da schnell Testdaten zusammen “klicken”.

(kurz und knapp :))

Sehe ich ähnlich, wobei meine Unittests wirklich nur eine Klasse als „Unit“ testen, der Rest wird meist gemockt, Ausnahmen sind u.a. java.lang.String :wink:

Vielen Dank für den Tipp mit Jailer, sieht Interessant aus.

Erstellt ihr damit eine Testfixture für mehrere Tests oder pro Test einzeln?

Pro Test-Klasse definieren wir die Testdaten. Wir hatte das eine zeitlang auch pro Test gemacht, mussten allerdings feststellen, dass durch die Anzahl der Tests ein Fullbuild mit Integrations-Tests einfach zu lange gedauert hat. Funktioniert im Zusammenspiel mit den Unit-Tests ganz gut.

Bei BDD wo man die Szenarios mit Gherkin beschreibt, sind dort im Szenario gleich die jeweiligen Beispieldaten dabei. Dann nur mehr die passende Methode, welche die Datenbank für die jeweiligen Szenarien füllt.

Das klingt gut, ist aber eine Ebene zu hoch, ich interessiert wie ihr die Testdaten für eure Integrationstest erzeugt.

Also in der Testpyramide für die Test der „mittleren“ Ebene, auch „Service Tests“ genannt, funktionales Testen im Sinne von BDD mit Cucumber (soll hier übrigens auch eingeführt werden) ist sicher gut und wichtig, aber leider eine Eben zu hoch für meine Frage da es die „Spitze“ der Pyramide betrifft :slight_smile:

Das ist es aber. BDD kann man auch für Integrationstests/funktionale Tests verwenden. In meinen Fall PHP Webapplikationen mit Behat und Mink testen lassen. Für jedes Feature gibt es eine Art Background, welcher beschreibt wie die Testdaten sind. Oder denke ich jetzt falsch und du meintest, wie man sich diese Testdaten ausdenkt?

Du kannst sowas im Endeffekt für alles hernehmen. Du musst nicht die ganze Applikation von oben bis unten durchintegriert testen. Kann auch von irgendwo anfangen. Im Background beschreibst du für deine Szenarien wie gerade die Datenbank aussehen muss und sie wird mit den analogen Methoden gefüllt. Dann kannst du deinen Service konkret testen.

Beispiel anhand von Sylius
[spoiler]

Feature: User account orders page
  In order to follow my orders
  As a logged user
  I want to be able to track and get an invoice of my sent orders

  Background:
    Given I am logged in user
      And I am on my account homepage
      And the following orders exist:
        | user                    | shipment                 | address                                                           |
        | sylius@example.com      | UPS, shipped, DTBHH380HG | Théophile Morel, 17 avenue Jean Portalis, 33000, Bordeaux, France |
        | ianmurdock@debian.org   | FedEx                    | Ian Murdock, 3569 New York Avenue, CA 92801, San Francisco, USA   |
        | ianmurdock@debian.org   | FedEx                    | Ian Murdock, 3569 New York Avenue, CA 92801, San Francisco, USA   |
        | linustorvalds@linux.com | DHL                      | Linus Torvalds, Väätäjänniementie 59, 00440, Helsinki, Finland    |
        | linustorvalds@linux.com | DHL                      | Linus Torvalds, Väätäjänniementie 59, 00440, Helsinki, Finland    |
        | sylius@example.com      | UPS                      | Théophile Morel, 17 avenue Jean Portalis, 33000, Bordeaux, France |
        | sylius@example.com      | UPS                      | Théophile Morel, 17 avenue Jean Portalis, 33000, Bordeaux, France |
        | linustorvalds@linux.com | DHL                      | Linus Torvalds, Väätäjänniementie 59, 00440, Helsinki, Finland    |
        | sylius@example.com      | UPS                      | Théophile Morel, 17 avenue Jean Portalis, 33000, Bordeaux, France |
        | sylius@example.com      | UPS                      | Théophile Morel, 17 avenue Jean Portalis, 33000, Bordeaux, France |
        | ianmurdock@debian.org   | FedEx                    | Ian Murdock, 3569 New York Avenue, CA 92801, San Francisco, USA   |
      And order #000000001 has following items:
        | product  | quantity |
        | Mug      | 2        |
        | Sticker  | 4        |
        | Book     | 1        |

Und dazu gibt es dann die analogen Methoden, die diese Daten in die Datenbank reinschreiben:

    /**
     * @Given /^there are following orders:$/
     * @Given /^the following orders exist:$/
     * @Given /^there are orders:$/
     * @Given /^the following orders were placed:$/
     */
    public function thereAreOrders(TableNode $table)
    {
        $manager = $this->getEntityManager();
        $orderRepository = $this->getRepository('order');
        $shipmentProcessor = $this->getContainer()->get('sylius.processor.shipment_processor');

        $currentOrderNumber = 1;
        foreach ($table->getHash() as $data) {
            $address = $this->createAddress($data['address']);

            /* @var $order OrderInterface */
            $order = $orderRepository->createNew();
            $order->setShippingAddress($address);
            $order->setBillingAddress($address);

            $order->setUser($this->thereIsUser($data['user'], 'sylius'));

            if (isset($data['shipment']) && '' !== trim($data['shipment'])) {
                $order->addShipment($this->createShipment($data['shipment']));
            }

            $order->setNumber(str_pad($currentOrderNumber, 9, 0, STR_PAD_LEFT));
            $this->getService('event_dispatcher')->dispatch('sylius.order.pre_create', new GenericEvent($order));

            $order->setCurrency('EUR');
            $order->complete();

            $shipmentProcessor->updateShipmentStates($order->getShipments(), ShipmentTransitions::SYLIUS_PREPARE);

            $manager->persist($order);

            $this->orders[$order->getNumber()] = $order;

            ++$currentOrderNumber;
        }

        $manager->flush();
    }

    /**
     * @Given /^order #(\d+) has following items:$/
     */
    public function orderHasFollowingItems($number, TableNode $items)
    {
        $manager = $this->getEntityManager();
        $orderItemRepository = $this->getRepository('order_item');

        $order = $this->orders[$number];

        foreach ($items->getHash() as $data) {
            $product = $this->findOneByName('product', trim($data['product']));

            /* @var $item OrderItemInterface */
            $item = $orderItemRepository->createNew();
            $item->setVariant($product->getMasterVariant());
            $item->setUnitPrice($product->getMasterVariant()->getPrice());
            $item->setQuantity($data['quantity']);

            $order->addItem($item);
        }

        $order->calculateTotal();
        $order->complete();

        $this->getService('sylius.order_processing.payment_processor')->createPayment($order);
        $this->getService('event_dispatcher')->dispatch('sylius.cart_change', new GenericEvent($order));

        $manager->persist($order);
        $manager->flush();
    }

[/spoiler]

Denke wir reden an einander vorbei :slight_smile:

BDD ist für funktionale Tests, die natürlich auch Integrationstests sind.
In der Pyramide (http://www.mountaingoatsoftware.com/blog/the-forgotten-layer-of-the-test-automation-pyramid) wäre das die Spitze, also UI Tests (das „User Interface“ könnte zB. auch nur aus Webservices bestehen).

Mich aber interessiert wie ihr eure „Servicetests“ aufsetzt, speziell wie die Testdaten in die DB kommen.
Dazu gehören IMHO zB. auch die DAO Tests, wo die CRUD Methoden getestet werden, es könnten aber auch einzelne Services getestet werden.
Die Testszenarien bei diesen Servicetests sind viel kleiner als bei funktionalen Tests, man braucht wengier Testdaten, tested aber auch nicht die ganze Story.

Ja auch mit BDD. Anstatt die UI schreibt man halt Szenarien für seine DAOs/Services, was soll nun in der Datenbank landen, wenn man diese oder jene Methode aufruft. Oder man hat das und dies in der DB und beschreibt was diese oder jene Methode nun zurückgeben muss :stuck_out_tongue:

Dann gibt’s noch SpecBDD, also Spezifikationen, was eine Klasse können muss, dies ähnelt aber eher dem Unittesting, da man hier wieder mockt und keine Integration fahrt.

Ok, wie landen denn die Daten in der DB?
Ist das speziell für PHP oder etwas das man auch von Java aus verwenden könnte?

Ganz simpel über einen ORM, im obigen Beispiel (siehe den Spoiler) Doctrine. Also die Klasse über ein Repository erstellt, über Setter befüllt und mit dem ORM in die DB gespeichert. Wenn man jetzt natürlich die DAOs testet ist es natürlich angebrachter die Examples die man reinbekommt direkt in SQL Statements zu parsen… Ob das dann sauber ist, gute Frage :stuck_out_tongue:

Danke, jetzt hab ich das auch entdeckt, PHP ist nicht so meins :wink:
Also im Endeffekt werden die DAOs genutzt um Testdaten zu erstellen.
Sehe da keine „Assertions“ oder ähnliches, wird wohl dann auf der BDD Seite geprüft.

Nehmt ihr keine speziellen Builder/Facories oder ähnliches um Testdaten zu erstellen?

Genau, das diente jetzt nur zum Erstellen der Daten aus dem Background. Getestet wird dann in den Scenarios, wo auch die Assertions liegen.

An welche Builder/Factories denkst du da? Es gibt ein EntitiyRepository, welches mittels createNew() die konkrete Klasse instanziert, die das vereinbarte Interface implementiert.

Naja, wenn man mal etwas komplexere Scenarien (noch nicht mal für Funktionale tests) zum testen braucht, wo zB. Daten in mehereren Tabellen vorliegen müssen, ist es recht müsig das alles nur per DAOs/Repositories zu machen.
Da helfen Builder, denen übergibt man nur die für den Test wirklich relevanten Daten, die anderen Daten, die zwar vorhanden sein müssen, aber nicht den Test selber beeinflussen, werden von den Buildern als Default werte erzeugt.

Um bei deinem Order Beispiel zu bleiben, sowas wie einen “Customer” wird es wohl auch geben müssen, ist aber nicht wieter relevant für die Order tests, abgesehen davon das er existieren muss.
Nimmt man immer denselben Customer für alle Tests, wird die Wahrscheinlichkeit der Interaktion zwischen Tests grösser (besonders beim parallelisieren der Tests).

Sehe da in deinem Beispiel keine “elegante” Möglichkeit den Customer anzulegen, ausser per DAO/Repository, aber dann wirkt der Test schnell unübersichtlich, “Anonyme” und pro Test eindeutige Customer würden helfen IME.

Um auf das Beispiel zurückzukommen, stehen ja in der Order die User drinnen, die bestellt haben. Diese werden einfach an eine andere Methode übergeben, die diese anlegt, ja.

$order->setUser($this->thereIsUser($data['user'], 'sylius'));

Sehe hier kein Problem, da thereIsUser auch in anderen Szenarien gebraucht wird. Das ist das schöne an der Gherkin-Sprache, hier wird IMHO jede Zeile zu einen Methodenaufruf übersetzt. Aber ja, ansonsten dass der User existieren muss und die Referenz gesetzt wird, ist er hier z.b. nicht weiter vorhanden… Btw. ist das ganze hier so konfiguriert, dass nach jedem Szenario die Datenbank mit den Background neu geladen wird, also jedes Szenario beginnt clean.

Sieh es mal so:
Irgendwer ändert mal den Konstruktor für User, wieviele Test musst du abändern? :wink:
So ganz ohne „ObjectMother“/Builder/Factory würde ich das nur noch bei „Bonsai“ Projekten nutzen.

Da funktioniert bei einer handvoll Tests gut, aber bei 5000+ Tests sieht die Sache anders aus :wink:

Mir geht es eben darum, wie Leute die Erzeugung von Testdaten managen, in kleinen Systemen ist das fast egal, bei großen wird das zum großen Problem.
Die letzte Firma hatte zum Schluss ca. 6000 Integrationstests (nund nochmal soviele Unittests, aber die sind immer schnell genug), da war die einzige Möglichekit eben die Tests Parallel laufen zu lassen, dafür dürfen sich die nicht gegenseitig beeinflussen, jeder Test muss seine minimale Testfixture selbst erzeugen, darf keine anderen Tests beeinflussen.

Zu viele direkte Abhängigkeiten auf den Prod Code und dessen Implementierungsdetails - bestes Beispiel sind Konstruktoren - und man muss mehr im Test Code ändern als im Prod Code bei Anpassungen.
Dafür eben die Builder/Factories/Object Mother.

Mit Builder wird das erzeugen von Testdaten zum Kinderspiel, und komplexe Testdaten zu erzeugen sehe ich oft als Begründung warum eben nicht alle Integrationstests auf Servicebene implementiert wurden die nützlich sein könnten bzw. wichtig sind.

[QUOTE=maki]Sieh es mal so:
Irgendwer ändert mal den Konstruktor für User, wieviele Test musst du abändern? ;)[/QUOTE]

Eine. Da es nur eine Methode gibt, die direkt auf das UserRepository zugreift und den User erstellt. Alle anderen verweisen benutzen nur diese Builder-Methode thereIsUser() :slight_smile: Btw. wäre der Konstruktur sogar egal, da gegen Interfaces gearbeitet wird :wink: Also bei diesen Änderungen müsste man die Builder ändern.

Ja, da stoßt Sylius mittlerweile auch an die Grenzen des 50 Minutenlimits von Travis…

Es erzeugt ja jedes Feature seine Testdaten für sich selbst… Aber in diesem Fall müsste man wirklich verschiedene DB’s zum parallelisieren verwenden. Wie habt ihr das gelöst?

Ahh… also doch, nix für ungut :slight_smile:
Wie gesagt mit PHP bin ich nicht so dicke, kenn mich da gar nicht aus, nicht persönlich nehmen wenn ich den Code falsch interpretiere, denke aber dass wir uns mittlerweile immer besser verstehen.

Damals war unsere Lösung u.a. wirklich für jeden Test einzigartige Daten zu erzeugen.
Oft reicht es schon einen eigenen Customer zu erzeugen (oder zumindest ist es ein guter Anfang), damit sich die Tests nicht in die Quere kommen. Weitere Testdaten dürfen sich auch nciht mit den Testdaten von anderen Tests überlappen bzw. kollidieren.
Jeder Test erstellte eben seine eigene (minimale) Fixture, ganze DB löschen ist nicht, stattdessen stellt die „setup“ Methode des Testcases oder der Testcase selber seine Daten und sorgt dafür dass diese in der DB sind (bei DBUNit wäre das sowas wie ein update_refresh oder so ähnlich), teardown ändert die Daten selber nie in der DB, „good setup doesn’t require a teardown“.

Die Tests die fiese Sachen machen wie DB oder Services runterfahren etc. kamen in eine seperate Gruppe für tests die sich nicht parallelisieren lassen.

Eine In-Memory DB ist nett zum lokalen Testen, aber auf den CI Machinen sollten die richtige DB verwendet werden, da half es ungemein eine lokale Installation auf dem CI Server der DB zu nehmen (Netzwerklatenzen vermeiden) und diese (PostgreSQL) von der Ramdisk laufen zu lassen (Disk IO vermeiden).
Ein Script welche den RDBMS Prozess abschiesst, die Ramdisk löscht und danach eine neue Instanz anlegt inkl. DB Account etc., lief vor dem Testlauf.
6000 Integrationstests seriell in 6-8 Stunden → Parallel dann 2-2,5 Stunden → DB auf lokale Ramdisk dann in 25 Minuten.

Der enorme Aufwand bestand meist nur darin die Tests Parallel lauffähig zu machen, das war das größte Problem.

Cucumber ist keine PHP-Angelegenheit.

Mit Cucumber versucht man eine Sprache zu erstellen anhand derer man eine Spezifikation verfaßt.

Diese führt man dann mit dem entsprechenden Adapter aus, die es auch für java gibt
Given there is 1 user

Sucht sich dann in den Step-Definitionen die passsende Methode dazu aus, die dem Regulären Ausdruck entspricht.

public void there_is_users(int n) {
    for(int i = 0; i<n;i++) {
        createAUser();
    }
}```

Given there are 5 users würde dann ebenso funktionieren und 5 user erstellen.

Hat auch keiner behauptet :wink:

TheDarkRoses Beispielcode war in PHP.

Ja, eine DSL eben.

Schon klar, das beantwortet aber nicht die Frage wie die Testdaten in die DB kommen, und darum ging es mir.