Aufbauend auf den in meinem vorherigen Artikel über das Testen mit (und ohne) Abhängigkeiten besprochenen Unterschieden und Best Practices ist es wichtig, zwei grundlegende Objekttypen beim Testen zu berücksichtigen: Datentransferobjekte (DTOs) und Wertobjekte.
Von technischer zu fachlicher Motivation
DTOs sind in der Regel technisch motiviert: Sie erleichtern den Datenaustausch zwischen Schichten, APIs oder Systemen und vereinfachen oder verändern häufig Entitäten für die Serialisierung und Übertragung. Normalerweise haben sie keine Domänenlogik, sondern sind eher Container, deren Hauptaufgabe darin besteht, die Daten zu strukturieren, anstatt Verhaltensregeln durchzusetzen. Die einzige Logik, die sie haben sollten, ist die der Selbstvalidierung. Denn meiner Meinung nach sollten sie sich selbst validieren, genauso wie Wertobjekte das tun.
Im Gegensatz dazu sind Wertobjekte geschäftsorientiert. Sie repräsentieren grundlegende Domänenkonzepte wie Währungen, Datumsangaben, Bereiche und Maße. Sie werden rein aufgrund ihrer Werte als gleichwertig angesehen, nicht aufgrund ihrer Identität oder Zustandsübergänge.
Unveränderlichkeit reduziert kognitive Last
Wie ich in meinem letzten Artikel erläutert habe, zählt zu den Hauptvorteilen von unveränderlichen Objekten, dass sie die kognitive Last beim Lesen und Verstehen von Code verringern. Das gilt auch für Testcode: Wenn ein Objekt unveränderlich ist, müssen wir uns keine Sorgen mehr darüber machen, dass sich sein interner Zustand während des Testens ändert.
Unveränderliche Wertobjekte werden einmal erstellt und ändern sich nie, wodurch die Einhaltung von Regeln der Domäne garantiert wird. Das macht sie ideal für Testszenarien, in denen bestimmte Werte überprüft oder verglichen werden müssen. Dank ihrer Vorhersehbarkeit können Entwicklerinnen und Entwickler sicher damit arbeiten, da sie wissen, dass es keine versteckten Nebenwirkungen geben kann.
DTOs sind jedoch oft veränderlich, da es technisch notwendig sein kann, Felder aus deserialisierten Datenquellen optional zu setzen oder zu füllen. Sind sie veränderlich, ist ständige Wachsamkeit erforderlich, um unbeabsichtigte Zustandsänderungen an DTOs während der Tests zu vermeiden.
Die Veränderlichkeit von DTOs sollte vermieden werden. Wenn wir sie beispielsweise als unveränderliche Container mit schreibgeschützten Eigenschaften implementieren, ergeben sich die Vorteile von Wertobjekten und Tests, die DTOs verwenden, sind weniger fehleranfällig.
Testcode, der mit unveränderlichen Objekten arbeitet, ist einfacher zu lesen und zu schreiben, da das Risiko entfällt, dass geteilte Referenzen Werte im Hintergrund verändern. Dies führt oft zu Fehlern und Verwirrung.
Test Doubles
In meinem letzten Artikel habe ich ausführlich über die Vorteile von gut gestalteten Test Doubles, Test Stubs und Mock Objects, geschrieben. Mit diesen Hilfsmittel können Abhängigkeiten isoliert und die Absicht von Tests klarer gemacht werden. Wichtig:
Unveränderliche Wertobjekte benötigen weder Stubbing noch Mocking. Sie stehen für feste Daten und haben keine zusammenwirkenden Abhängigkeiten. Dadurch unterscheiden sie sich grundlegend von Service- oder Entity-Abhängigkeiten. Wenn ein Test ein Wertobjekt verwendet, dann kommt immer das echte Objekt zum Einsatz, sodass kein Test Double erforderlich ist.
DTOs erfordern in der Regel ebenfalls keine Test Doubles, da es sich um einfache Container handelt. Enthält ein DTO jedoch Logik oder interagiert er mit anderen Komponenten, kann ein Test Double erforderlich sein, um abhängige Verhaltensweisen zu isolieren.
Objekte, die aus technischen Gründen erstellt werden (DTOs), werden in der Regel an Grenzen wie Serialisierung, Transport oder API-Schnittstellen verwendet. Objekte, die aus Domänenanforderungen entstehen (Wertobjekte), drücken die Konzepte aus, die das Rückgrat der Geschäftslogik bilden. Dank der Stabilität und Einfachheit unveränderlicher Objekte können sich Entwicklerinnen und Entwickler in Tests auf die Überprüfung des Verhaltens konzentrieren, ohne sich um indirekte Nebenwirkungen kümmern zu müssen.
Empfehlung und Fazit
Verwende wann immer möglich unveränderliche Objekte. Egal, ob in der Domäne, in der Infrastruktur oder an den Grenzen zwischen diesen Kontexten: Unveränderlichkeit sorgt für Konsistenz in Tests und macht komplexe Setups oder fragile Zusicherungen überflüssig.
Ersetze Wertobjekte in deinen Tests nicht durch Test Stubs oder Mock Objects, sondern verwende echte Instanzen, um die Korrektheit zu garantieren. Da Wertobjekte nur einen festen Wert repräsentieren, gibt es nichts, von dem du den zu testenden Code isolieren musst.
Wenn dein Framework dies unterstützt, solltest du DTOs als unveränderlich entwerfen. Dadurch erhalten DTOs viele der praktischen Vorteile von Wertobjekten und die Lücke wird geschlossen.
Das Testen wartbaren, lesbaren Codes erfordert eine Grundlage aus klaren Absichten und vorhersehbarem Verhalten. Wie im vorherigen Artikel besprochen, spiegelt die Entwicklung von Test Doubles in PHPUnit die zunehmende Klarheit der Praktiken für das Testen von Software wider. In ähnlicher Weise macht der sinnvolle Einsatz unveränderlicher Wertobjekte und sorgfältig entworfener DTOs das Stubbing oder Mocking solcher Objekte überflüssig.
Wenn technische und domänenbezogene Motivationen explizit gemacht werden und Unveränderlichkeit wo immer möglich gewählt wird, können Entwicklerinnen und Entwickler robuste und verständliche Tests erstellen, die frei von der Angst vor versteckten Zustandsänderungen sind.