Unit Tests für komplizierte Systeme



  • Moin,

    ich bin momentan am Verzweifeln 🙂
    Und zwar habe ich ein ACL-System mit beliebig vielen Gruppenzugehörigkeiten auf beliebig vielen Ebenen geschrieben. Sprich es gibt sowohl von zugreifenden Objekten als auch von Ressourcen eine komplette Baumhierarchie mit Vererbung.
    Nun hatte ich das Gefühl, dass das Ding nicht immer so funktionierte, wie es sollte.
    Also habe ich das nochmal als PHPUnit-Unittest nachprogrammiert: In dem Fall ähnelt das eher einem funktionalen Test, da ich eine Unmenge an Datensätzen erzeuge, die dann mit der Library verarbeite und teste, ob mein Script das gleiche herausbekommt wie die Library.
    Außerdem habe ich angefangen, die Library in viele Einzelteile zu zerlegen, um die dann wiederum mit kleinen Unittests testen zu können.

    Beide Teile sind allerdings so kompliziert, dass ich mich tatsächlich fragen muss, wie ich sicherstellen kann, dass der UnitTest an sich korrekt arbeitet.
    Das Problem ist hierbei, dass das Script nur ganz selten anderer Meinung als die Library ist - so alle 200 Operationen. Da die Datenbank zu dem Zeitpunkt schon hinreichend kompliziert ist, kann ich mir das nichtmehr einfach auf ein Blatt Papier malen und entscheiden, wer hier Recht hat: Datenbank oder Script (das habe ich am Anfang noch gemacht ^^ )
    Ich kann zwar jedes Einzelteil der Library in einen UnitTest packen, jedoch hängt das Ganze zum Großteil an der Datenbank und die meisten Operationen werden auch direkt in der Datenbank ausgeführt. Heißt: Der Kontext durch die Datenbank spielt eine große Rolle in der Logik der Library selbst. Je nach Kontext kann sich die Library also bei gleichem Code richtig & falsch verhalten.

    Wie legt man solche funktionalen Tests an, sodass man relativ sicher sein kann, dass zumindest der Unit Test wie gewollt funktioniert? Wie testet man, günstigerweise datenbankgestützte Systeme, sodass man auch die Kontextabhängigkeit berücksichtigt? Wie trage ich dem Rechnung, dass ich alle möglichen Kontexte prüfe? Geht das noch anders als mit möglichst vielen Zufallskontexten?

    Mit freundlichen Grüßen,



  • Auf die Gefahr hin, wenig Konstruktives zu leisten:

    Ich kann zwar jedes Einzelteil der Library in einen UnitTest packen, jedoch hängt das Ganze zum Großteil an der Datenbank und die meisten Operationen werden auch direkt in der Datenbank ausgeführt. Heißt: Der Kontext durch die Datenbank spielt eine große Rolle in der Logik der Library selbst. Je nach Kontext kann sich die Library also bei gleichem Code richtig & falsch verhalten.

    Das ist aber doch gerade der Sinn von Unit-Tests. Kleinste testbare Einheiten (!) testen. Wenn diese Tests zu komplex werden, macht ihr entweder was bei eurer Architektur kaputt (zu viel Verantwortung an einer Stelle konzentriert?) oder euer Testprozess ist ein wenig aus der Bahn. Oder beides.

    Die Frage nach "gutem Testen" ist pauschal schwer zu beantworten.
    Grob sollte man versuchen, sich von unten nach oben durch die Architektur zu testen. Unit-Tests, Modultests (Module als Kollektion von Komponenten, die untereinander agieren), Subsystem-Tests (Subsysteme als Kollektion von Modulen, die Untereinander agieren), Systemtests (...) und Akzeptanztest (also Tests mit "echten" Daten). Unit-Tests allein reichen für größere Softwaresysteme bei Weitem nicht aus.

    Auf welcher Ebene die Interaktion zwischen deinen Komponenten und der Datenbank getestet wird, hängt von eurer Architektur ab.

    Die jeweiligen Tests kannst du als Blackboard-Tests oder Whiteboard-Tests formulieren. Gerade bei Datenbanken scheint mir ersteres angebracht (ohne Erfahrungswert, ist nur ein Gefühl), Stichworte hierzu sind Equivalence Partitioning und Boundary Analysis.

    Disclaimer:
    Ich habe keine Ahnung von ACLs und davon, mit welcher Art Datensätze man da so konfrontiert wird (und ob die oben vorgeschlagenen Teststrategien da überhaupt ansatzweise umsetzbar sind). Ich hoffe, das hilft trotzdem ein kleines bisschen weiter.



  • Moin,

    jede Antwort, in der noch ein Funken Wahrheit steckt, ist besser als keine.

    Das ist aber doch gerade der Sinn von Unit-Tests. Kleinste testbare Einheiten (!) testen. Wenn diese Tests zu komplex werden, macht ihr entweder was bei eurer Architektur kaputt (zu viel Verantwortung an einer Stelle konzentriert?) oder euer Testprozess ist ein wenig aus der Bahn. Oder beides.

    Das Problem hierbei ist ein bisschen, dass die auf Anwendungsebene atomaren Funktionen auf Datenbankebene in mehreren Schritten bestehen (natürlich in einer Transaction verpackt). Ich verwende für das System Modified Preordered Tree Traversal (kurz: Durchnummerierung aller Nodes von links nach rechts). Sobald ich jetzt z.B. anfange, eine Node von a nach b zu schieben, brauche ich dafür in meiner Implementierung genau 6 Update-Queries. In meiner Klasse ist das natürlich nur ein Aufruf: Tree::move()
    Alle Tests dazwischen würden auf einem mehr oder minder undefinierten Zustand ausgeführt werden, daher wären Tests zwischendrin extrem ekelhaft.
    Ich will jetzt gar nicht ausschließen, es kann ja durchaus sein, dass ich da schlechtes Design verbaut habe. Nur ich sehe einfach nicht, wie ich das weiter (sinnvoll) abstrahieren könnte.

    Ich habe angefangen, sowas teilweise im Unit-Test zu testen: Dabei musste ich dann feststellen, dass Dinge, die implizit durch die Datenbankbefehle durchgeführt werden, in einer Vergleichsimplementation sehr unschön werden. Wenn ich den Algorithmus nicht in den Test kopieren möchte, so muss ich ihn anders schreiben - das hat dann in meinem Fall dazu geführt, dass ich > 5 Zustände einer Node unterscheiden musste, wo ich beim Datenbanksystem einfach nur iterativ Queries ausführen muss. Das hat den Vorteil, dass es garantiert das ergibt, was ich haben möchte, und den Nachteil, dass es so komplizieret ist, dass es schon selbst wieder Fehler aufwerfen könnte 😛

    Die jeweiligen Tests kannst du als Blackboard-Tests oder Whiteboard-Tests formulieren. Gerade bei Datenbanken scheint mir ersteres angebracht (ohne Erfahrungswert, ist nur ein Gefühl), Stichworte hierzu sind Equivalence Partitioning und Boundary Analysis.

    Auf den unteren Ebenen verwende ich tatsächlich Whiteboard-Tests. Auf der obersten Ebene suche ich mir per Zufall Relationen heraus und frage dann das Script bzw. die Datenbank, wie die das sehen.
    Blackboard-Tests wären (meiner Vermutung nach) sehr ineffizient, da es sehr viele mögliche "falsche" Relationen gibt und nur sehr wenige "richtige" (also die, die tatsächlich genau so definiert wurden). Deswegen die Mischung als Zufallsprodukt.
    Equivalence Partitioning wird leider aufgrund der fehlenden Äquivalenz nix, ebenso wie Boundary Analysis, da es keine Boundaries gibt ;-(.

    Zu Kernproblem von oben: Wie sieht das denn genau aus, mit dem Test von inkonsistenten Datenbankzuständen? Das wäre ja die logische Folgerung, wenn man nur atomar testen möchte.

    Vielen Dank für die Antwort!

    Mit freundlichen Grüßen,



  • Alle Tests dazwischen würden auf einem mehr oder minder undefinierten Zustand ausgeführt werden, daher wären Tests zwischendrin extrem ekelhaft.

    Wenn du auf Anwenderebene nicht mehr teilen kannst, musst du auf der Ebene testen, auf der du wieder teilen kannst (soweit möglich).

    Weißt du inzwischen, wo diese Fehler auftreten? Meine Antwort war u.a. deshalb so unspezifisch, weil du einen Bug in deiner Software hast, von dem du nicht weißt, wo er herkommt. Da ist es schwer, konkreten Rat zu erteilen. Insbesondere das macht mir Sorgen:

    Je nach Kontext kann sich die Library also bei gleichem Code richtig & falsch verhalten.

    Wie meinst du das? Richtig und falsch schließen sich aus. Entweder deine Software funktioniert, oder eben nicht 🙂 Gibt es denn gar nichts, was die Datensätze gemeinsam charakterisiert, bei denen Unstimmigkeiten auftreten?

    Zu Kernproblem von oben: Wie sieht das denn genau aus, mit dem Test von inkonsistenten Datenbankzuständen? Das wäre ja die logische Folgerung, wenn man nur atomar testen möchte.

    Entschuldige, das müsstest du mir bitte nochmal erklären 🙂



  • nachträglich unittests für komplexe systeme zu schreiben, ist immer schwierig bis unmöglich. vor allem wenn noch drittsysteme wie datenbanken drin hängen.

    guter tipp fürs nächste mal: schreib die unittests nicht erst am ende sondern direkt von anfang an. das hat einen herrlichen effekt auf die systemarchitektur. du überlegst dir nämlich sofort, wie du TESTBAREN code schreiben kannst. das hat zur folge, dass der code einfacher und wartbarer wird mit weniger abhängigkeiten und seiteneffekten zwischen den einzelnen komponenten. hilft dir jetzt natürlich nicht wirklich weiter, wo das kind schon in den brunnen gefallen ist.



  • Weißt du inzwischen, wo diese Fehler auftreten? Meine Antwort war u.a. deshalb so unspezifisch, weil du einen Bug in deiner Software hast, von dem du nicht weißt, wo er herkommt. Da ist es schwer, konkreten Rat zu erteilen.

    Ich bin mittelerweile soweit, dass ich sagen kann, dass der Fehler irgendwo beim Löschen von Objekten auftritt. Wenn ich das nämlich deaktiviere, so läuft der Unit-Test mit > 3.000 Operationen ohne Probleme durch.

    Wie meinst du das? Richtig und falsch schließen sich aus. Entweder deine Software funktioniert, oder eben nicht 🙂 Gibt es denn gar nichts, was die Datensätze gemeinsam charakterisiert, bei denen Unstimmigkeiten auftreten?

    Damit war gemeint, dass der prinzipiell alles aus der Datenbank zieht, es verarbeitet, und anschließend wieder zurückschreibt.
    D.h. es kann sein, dass der Code bei einigen Datenbankzuständen vollständig richtig arbeitet und bei anderen eben nicht. => Kontextbezogenheit

    Entschuldige, das müsstest du mir bitte nochmal erklären

    Fällt dann weg, wenn ich das eh nicht testen muß (siehe deinen obersten Kommentar in deinem letzten Post) 🙂

    @lolhehe: Mach ich nächstemal garantiert. Ich schreib sowas _nie_ wieder im Nachhinein 🙂

    MfG



  • Warum schreibst du dein System nicht komplett neu?


Anmelden zum Antworten