Wie sieht ein moderner Test-Driven-Development-Ansatz (TDD) in Python aus?

Testen

Lass uns in die faszinierende Welt des testgetriebenen Entwicklungsansatzes (TDD) in Python eintauchen. In diesem Artikel lüftest du die Geheimnisse hinter effektiven Teststrategien und erfährst, wie du mit leistungsstarken Frameworks wie unittest und pytest gute Ergebnisse erzielen kannst. Lass dich von den Vorteilen von Continuous Integration (CI) und Continuous Deployment (CD) begeistern und entdecke, wie du deinen Entwicklungsprozess auf das nächste Level bringen kannst. Los geht’s.

Letztes Mal haben wir uns schlau gemacht, was Test Driven Development (TDD) überhaupt ist. Heute gucken wir in die Praxis.

Ein moderner testgetriebener Entwicklungsansatz (TDD) in Python umfasst in der Regel die folgenden Schritte:

  1. Einen Test schreiben: Bevor ein Entwickler Code schreibt, schreibt er einen Test für eine bestimmte Funktionalität. Der Test sollte sicherstellen, dass die erwartete Ausgabe erhalten wird, wenn bestimmte Eingaben gegeben werden.
  2. Führ den Test aus und sieh, ob er fehlschlägt: Der Test wird dann ausgeführt und sollte fehlschlagen, weil der entsprechende Code noch nicht geschrieben wurde.
  3. Schreib den Code: Der Entwickler schreibt dann den Code, um den Test erfolgreich zu machen. Dies sollte die minimale Menge an Code sein, die erforderlich ist, um den Test zu bestehen.
  4. Führ den Test aus und guck, ob er bestanden wird: Der Test wird dann erneut ausgeführt und sollte erfolgreich sein, da der Code so geschrieben wurde, dass er die erwartete Ausgabe erzeugt.
  5. Refactoring des Codes: Falls erforderlich, kann der Entwickler den Code umgestalten, um das Design zu verbessern und gleichzeitig sicherzustellen, dass die Tests weiterhin erfolgreich sind.
  6. Wiederholen: Der Prozess wird dann für jede neue Funktionalität wiederholt.

In Python gibt es mehrere beliebte Test-Frameworks wie unittest, pytest und nose, die zum Schreiben, Ausführen und Durchsetzen der Tests verwendet werden können. Diese Frameworks bieten mehrere Assertion-Methoden und Testfindungsmechanismen, die den TDD-Prozess effizienter machen.

Darüber hinaus werden in modernen TDD-Ansätzen häufig Tools wie Continuous Integration (CI) und Continuous Deployment (CD) eingesetzt, mit denen Entwickler die Tests automatisch ausführen und den Code in verschiedenen Umgebungen bereitstellen können. Dies kann dazu beitragen, Fehler in einem frühen Stadium des Entwicklungsprozesses zu erkennen und sicherzustellen, dass der Code stets in einem veröffentlichungsfähigen Zustand ist.

Ein Codebeispiel für einen modernen testgetriebenen Entwicklungsansatz in Python

Hier ist ein Beispiel für einen modernen testgetriebenen Entwicklungsansatz (TDD) in Python unter Verwendung des Unittest-Frameworks:

import unittest
class Calculator:
    def add(self, x, y):
        return x + y
class TestCalculator(unittest.TestCase):
    def test_add(self):
        calculator = Calculator()
        result = calculator.add(2, 2)
        self.assertEqual(result, 4)
if name == 'main':
    unittest.main()

In diesem Beispiel:

  1. Wir definieren zunächst eine Klasse Calculator mit einer einzigen Methode add(self, x, y), die zwei Argumente annimmt und die Summe davon zurückgibt.
  2. Dann definieren wir eine Klasse TestCalculator, die von unittest.TestCase erbt. Darin haben wir eine Methode test_add(), die eine Instanz der Calculator-Klasse erzeugt und die add-Methode mit den Argumenten 2 und 2 aufruft.
  3. Die Methode test_add() bestätigt mit self.assertEqual(result, 4), dass das Ergebnis des Aufrufs der Methode add gleich 4 ist.
  4. Schließlich wird der Test durch den Aufruf von unittest.main() ausgeführt.

Wenn der Test zum ersten Mal ausgeführt wird, schlägt er fehl, weil die Klasse Calculator noch nicht über die implementierte Additionsmethode verfügt. Sobald die Methode implementiert ist, sollte der Test gelingen.

Dies ist ein einfaches Beispiel zur Veranschaulichung des TDD-Konzepts. In der Praxis umfassen die Tests in der Regel ein breiteres Spektrum an Szenarien, einschließlich Randfällen und Fehlerbehandlung. Und die Implementierung wird komplexer sein und mehrere Methoden und Klassen umfassen. Außerdem sollte der Code regelmäßig refaktorisiert werden, um sicherzustellen, dass er wartbar und lesbar bleibt.

Und wie bereits erwähnt, ist der Einsatz von Tools wie Continuous Integration (CI) und Continuous Deployment (CD) in einem realen Szenario sehr zu empfehlen, um sicherzustellen, dass der Code immer in einem veröffentlichungsfähigen Zustand ist und Fehler frühzeitig im Entwicklungsprozess erkannt werden.

Ein komplexeres Beispiel, um das Konzept von TDD in Python-Code zu veranschaulichen

Ein komplexeres Beispiel für einen modernen testgetriebenen Entwicklungsansatz (TDD) in Python unter Verwendung des pytest-Frameworks:

import pytest
from my_module import MyClass
class TestMyClass:
    @pytest.fixture
    def my_class_instance(self):
        return MyClass()
    def test_my_class_addition(self, my_class_instance):
        assert my_class_instance.add(2, 2) == 4

    def test_my_class_subtraction(self, my_class_instance):
        assert my_class_instance.subtract(4, 2) == 2

    def test_my_class_multiplication(self, my_class_instance):
        assert my_class_instance.multiply(2, 3) == 6

    def test_my_class_division(self, my_class_instance):
        assert my_class_instance.divide(6, 2) == 3

In diesem Beispiel:

  1. Wir importieren die MyClass aus my_module, das wir testen wollen.
  2. Dann definieren wir eine Klasse TestMyClass, die nicht von einer Basisklasse erbt. In ihr haben wir 4 Methoden test_my_class_addition, test_my_class_subtraction, test_my_class_multiplication, test_my_class_division, die die grundlegenden mathematischen Operationen der Klasse MyClass testen.
  3. Wir benutzen pytest.fixture, um eine Fixture zu erstellen, die eine Instanz von MyClass zurückgibt. Diese Instanz wird als Parameter an die Testmethoden übergeben, was ihnen erlaubt, die Methoden der Klasse aufzurufen.
  4. Jede Testmethode behauptet, dass der von der entsprechenden Methode von MyClass zurückgegebene Wert gleich dem erwarteten Wert ist, z. B. mit assert my_class_instance.add(2, 2) == 4.

Wenn die Tests ausgeführt werden, schlagen sie fehl, weil in der Klasse MyClass die Methoden add, subtract, multiply und divide noch nicht implementiert sind. Sobald die Methoden implementiert sind, sollten die Tests erfolgreich sein.

Dieses Beispiel zeigt, wie man eine Klasse mit mehreren Methoden testet und wie man mit Hilfe von Fixtures, einer Funktion des pytest-Frameworks, ein gemeinsames Setup für alle Testmethoden verwendet. Außerdem zeigt dieses Beispiel, wie man Assertions verwendet, um die erwarteten Ergebnisse der Methodenaufrufe zu testen.

Es ist wichtig zu beachten, dass die Tests ein breites Spektrum an Szenarien abdecken sollten, einschließlich Randfällen und Fehlerbehandlung, um sicherzustellen, dass die Implementierung korrekt und robust ist. Außerdem sollte der Code regelmäßig refaktorisiert werden, um sicherzustellen, dass er wartbar und lesbar bleibt.

Wie würde die tatsächliche Implementierung von „MyClass“ aussehen?

Hier ist ein Beispiel dafür, wie die Implementierung von „MyClass“ auf der Grundlage der bereitgestellten Tests aussehen könnte:

class MyClass:
    def add(self, x, y):
        return x + y

    def subtract(self, x, y):
        return x - y

    def multiply(self, x, y):
        return x * y

    def divide(self, x, y):
        return x / y

Dies ist eine einfache Implementierung, die den grundlegenden mathematischen Operationen (Addition, Subtraktion, Multiplikation und Division) entspricht, auf die die Tests geprüft wurden.

Es ist wichtig zu beachten, dass die Implementierung so einfach wie möglich sein sollte und nur den notwendigen Code enthalten sollte, um die Tests zu bestehen. Außerdem sollte die Implementierung so allgemein wie möglich gehalten werden, so dass sie eine Vielzahl von Eingaben und Szenarien verarbeiten kann.

Es ist auch wichtig, die Randfälle und die Fehlerbehandlung zu berücksichtigen und sie mit den Testfällen zu testen. Sie könnten beispielsweise einen Testfall hinzufügen, um zu prüfen, was passiert, wenn Sie durch Null dividieren oder nicht-numerische Werte als Eingabe übergeben. Und Sie sollten diese Fälle in der Implementierung behandeln, indem Sie Ausnahmen auslösen oder entsprechende Meldungen zurückgeben, je nach den Anforderungen der Anwendung.

Denken Sie daran, dass der TDD-Ansatz ein iterativer Prozess ist und dass sich die Implementierung und die Tests im Laufe der Zeit weiterentwickeln werden, um die sich ändernden Anforderungen der Anwendung zu erfüllen.

Was sind die Unterschiede zwischen den Frameworks „unittest“ und „pytest“?

unittest und pytest sind beides beliebte Test-Frameworks für Python, aber sie haben einige wesentliche Unterschiede:

  • Syntax: unittest folgt einem eher traditionellen xUnit-Stil des Testens, der die Erstellung einer Klasse für jedes Testmodul und von Methoden für jeden Testfall erfordert. Im Gegensatz dazu verwendet pytest eine einfachere, prägnantere Syntax, die es Ihnen erlaubt, Testfunktionen direkt zu schreiben, ohne Klassen oder Methoden erstellen zu müssen.
  • Assertions: unittest verwendet eine eingebaute Assertion-Methode, assertEqual(a, b), während pytest die eingebaute assert-Anweisung verwendet, die für viele Entwickler vertrauter und lesbarer ist.
  • Fixtures: pytest hat einen eingebauten Fixture-Mechanismus, der es erlaubt, wiederverwendbare Objekte zu erstellen und sie zwischen Testfunktionen zu teilen. Dies kann nützlich sein, um Code-Duplizierung zu reduzieren und Tests modularer zu gestalten. unittest hat keine eingebaute Unterstützung für Fixtures, aber Sie können setUp- und tearDown-Methoden verwenden, um Testdaten einzurichten und zu bereinigen.
  • Plugins: pytest verfügt über ein reichhaltiges Ökosystem von Plugins, mit denen die Funktionalität erweitert werden kann, z.B. für die Testfindung, die Testausführung und die Testberichterstattung. unittest verfügt nicht über so viele eingebaute Möglichkeiten für Plugins, aber Sie können Bibliotheken von Drittanbietern verwenden, um die gleiche Funktionalität zu erreichen.
  • Gleichzeitigkeit: pytest kann Tests automatisch erkennen und parallel ausführen, was die Testausführung erheblich beschleunigen kann. unittest hat keine eingebaute Unterstützung für Gleichzeitigkeit, aber Sie können Bibliotheken von Drittanbietern verwenden, um die gleiche Funktionalität zu erreichen.

Fazit

Fassen wir das Ganze mal zusammen: unittest ist traditioneller und erfordert mehr Boilerplate-Code, aber es kann eine gute Wahl für Projekte sein, die einer bestimmten Struktur folgen oder strenge Richtlinien einhalten müssen. Auf der anderen Seite ist pytest moderner und hat eine einfachere Syntax sowie ein reichhaltiges Ökosystem von Plugins, was es flexibler und leistungsfähiger macht.

Photo by Shopify Partners from Burst