Anstatt mitzumachen
Unittest ist wahrscheinlich das bekannteste Testwriting-Framework in Python. Es ist sehr einfach zu erlernen und einfach in Ihrem Projekt zu verwenden. Aber nichts ist perfekt. In diesem Beitrag möchte ich über eine Funktion sprechen, die mir persönlich (ich glaube nicht eine) an Unittest fehlt.
Ein bisschen über Unit-Tests
Bevor ich das Test-Framework diskutiere (und verurteile), halte ich es für notwendig, ein wenig über das Testen im Allgemeinen zu sprechen. Als ich den Ausdruck „Unit Testing“ zum ersten Mal hörte, dachte ich, dass es in der Verantwortung eines Qualitätskontrolldienstes liegt, der die Softwaremodule auf Übereinstimmung mit den Anforderungen überprüft. Stellen Sie sich meine Überraschung vor, als ich herausfand, dass dieselben Tests von Programmierern geschrieben werden sollten. Anfangs habe ich überhaupt keine Tests geschrieben. Nichts kann diese Empfindungen ersetzen, wenn Sie morgens aufwachen und vom Benutzer lesen: „Das Programm funktioniert nicht. Es muss dringend etwas getan werden. “ Zuerst schien es mir, dass dies ein völlig normaler Prozess war, bis ich den ersten Unit-Test schrieb, bei dem ein Fehler festgestellt wurde. Unit-Tests scheinen im Allgemeinen genau dann nutzlos zu sein, bis sie Probleme erkennen. Jetzt kann ich einfach keine neue Funktion festlegen, ohne einen Test darauf zu schreiben.
Tests werden jedoch nicht nur verwendet, um zu überprüfen, ob der Code korrekt geschrieben wurde. Hier ist eine Liste von Funktionen, die meiner Meinung nach Tests durchführen:
- Erkennung von Fehlern im Programmcode
- Geben Sie Programmierern das Vertrauen, dass ihr Code funktioniert
- Folgerung aus dem vorherigen Absatz: Die Möglichkeit, das Programm sicher zu ändern
- Tests - eine Art Dokumentation, die das Verhalten des Systems am besten beschreibt
In gewisser Weise wiederholen Tests die Struktur des Programms. Sind die Grundsätze für die Erstellung von Programmen auf Tests anwendbar? Ich glaube das ja - das ist das gleiche Programm, obwohl es ein anderes testet.
Problembeschreibung
Irgendwann kam mir die Idee, einen abstrakten Test zu schreiben. Was meine ich damit? Dies ist ein Test, der sich nicht selbst ausführt, sondern Methoden deklariert, die von den in den Erben definierten Parametern abhängen. Und dann entdeckte ich, dass ich dies nicht menschlich in unittest tun kann. Hier ist ein Beispiel:
class SerializerChecker(TestCase): model = None serializer = None def test_fields_creation(self): props = TestObjectFactory.get_properties_for_model(self.model) obj = TestObjectFactory.create_test_object_for_model(self.model) serialized = self.serializer(obj) self.check_dict(serialized.data, props)
Ich denke, auch ohne die Implementierung von TestObjectFactory und der check_dict-Methode zu kennen, ist klar, dass Requisiten ein Wörterbuch mit Eigenschaften für ein Objekt sind. Obj ist ein Objekt, für das wir den Serializer überprüfen. check_dict überprüft Wörterbücher rekursiv auf Übereinstimmung. Ich denke, viele Leute, die mit unittest vertraut sind, werden sofort sagen, dass dieser Test nicht meiner Definition von abstrakt entspricht. Warum? Weil die Methode test_fields_creation von dieser Klasse ausgeführt wird, die wir absolut nicht benötigen. Nach einiger Informationssuche kam ich zu dem Schluss, dass die am besten geeignete Option nicht darin besteht, den SerializerChecker von TestCase zu erben, sondern die Erben irgendwie zu implementieren:
class VehicleSerializerTest(SerializerChecker, RecursiveTestCase): model = Vehicle serializer = VehicleSerialize
RecursiveTestCase ist ein Nachkomme von TestCase, der die check_dict-Methode implementiert.
Diese Lösung ist aus mehreren Positionen gleichzeitig hässlich:
- In der SerializerChecker-Klasse müssen wir wissen, dass das Kind von TestCase erben muss. Diese Abhängigkeit kann Probleme für diejenigen verursachen, die mit diesem Code nicht vertraut sind.
- Die Entwicklungsumgebung glaubt hartnäckig, dass ich falsch liege, da SerializerChecker keine check_dict-Methode hat
Fehler, den die Entwicklungsumgebung auslöstEs scheint, dass Sie einfach einen Stub für check_dict hinzufügen können und alle Probleme behoben wurden:
class SerializerChecker: model = None serializer = None def check_dict(self, data, props): raise NotImplementedError def test_fields_creation(self): props = TestObjectFactory.get_properties_for_model(self.model) obj = TestObjectFactory.create_test_object_for_model(self.model) serialized = self.serializer(obj) self.check_dict(serialized.data, props)
Dies ist jedoch keine vollständige Lösung des Problems:
- Tatsächlich haben wir eine Schnittstelle erstellt, die keinen Nachkommen dieser Klasse implementiert, sondern RecursiveTestCase, die vernünftige Fragen für die Architektur stellt.
- TestCase verfügt über viele assert * -Methoden. Müssen wir wirklich für jeden verwendeten einen Stub schreiben? Scheint immer noch eine gute Lösung zu sein?
Zusammenfassend
Unittest bietet nicht die vernünftige Möglichkeit, eine von TestCase geerbte Klasse zu "trennen". Ich würde mich sehr freuen, wenn eine solche Funktion zum Framework hinzugefügt würde. Wie lösen Sie dieses Problem?