Grafik zur Visualisierung des Test-Driven-Development-Zyklus mit den Schritten Think, Test, Code und Refactor um eine physikalische Wärmeleitungsformel und ihre Randbedingungen.

Berechnung läuft - die Formel als Service-Modul

heat-conduction-app: Referenzprojekt für moderne Backend- und Webarchitektur

Test-Diven-Development (TDD) Backend Softwarearchitektur Flask heat-conduction-app

← Zurück zur Übersicht | ← Meilenstein 1 - Projektgerüst

Was wurde bisher umgesetzt

Der erste Schritt bestand in der Ausarbeitung des Problem- bzw. Aufgabenraums mit dem Ingenieurproblem als fachlichem Kern und den funktionalen Bausteinen als Denkstruktur. Dies habe ich im Einführungspost - Teil 2 näher beschrieben.

Im zweiten Schritt wurde innerhalb des Lösungsraums eine geeignete Softwarearchitektur sowie ein sinnvoller Software-Stack ausgewählt. Zusätzlich wurde ein 4-Phasen-Modell eingeführt, das architektonische Evolutionsstufen definiert. Details dazu finden sich im Einführungspost - Teil 3.

Innerhalb dieser ersten Schritte wird noch kein Code geschrieben, die Umsetzung in Code startet mit der Implementierungsebene. Auch hier müssen zunächst grundlegende Entscheidungen bezüglich der Applikationsstruktur und des Projektsetup getroffen werden. Wie soll die Anwendung innerhalb des Frameworks organisiert werden? Sollen Verantwortlichkeiten wie Logik, Datenhandling und externe Kommunikation getrennt werden oder soll die fachliche Domäne als eigenständige Einheit innerhalb der Webanwendung strukturiert werden? Am Ende dieses Schritts steht das Projektgerüst als klar definierter Startpunkt für die eigentliche Umsetzung. Ausführliche Informationen dazu finden sich Meilenstein 1 - Projektgerüst.

Teil 1 - Backend strukturell funktionsfähig machen

Mit der Erstellung des Projektgerüst wurden nicht nur die Ordner und Packages angelegt, sondern auch zahlreiche Module vorbereitet, die zunächst überwiegend mit Kommentaren versehen sind.

Dieser Meilenstein ist zweigeteilt zunächst geht es darum zu zeigen dass die Projektstruktur funktional zusammenspielt und die Architektur korrekt verankert ist. Dazu werden drei minimale Schritte umgesetzt:

  1. Main-Blueprint anlegen (Container für die Fachdomäne)
  2. Dummy-Route /conduction erstellen
  3. Minimaltest zur Verifikation der Integration

Ziel ist zu zeigen, dass:

  • Modul existiert
  • Route ist erreichbar
  • Blueprint ist korrekt registriert
  • Flask-Applikation läuft

Tests fungieren hier bewusst als technischer Nachweis (“Proof-of-Work”) der Architektur. Die Vorgehensweise orientiert sich dabei am Test-Driven-Development-Ansatz.

Blueprint anlegen und registrieren

Zunächst wird ein main-Blueprint erstellt und im Modul app/main/__init__.py definiert:

bp = Blueprint("main", __name__)

from app.main import routes

Dieser Blueprint bildet später den Container für die fachliche Domäne innerhalb der Anwendung. Damit der Blueprint aktiv wird, muss er innerhalb der App-Factory registriert werden:

def create_app(config=Config) -> CoreApp:
    app = CoreApp(__name__)

    ...

    from app.main import bp as main_bp
    app.register_blueprint(main_bp)

    return app

Damit ist sichergestellt, dass alle Routes des Blueprints im Application-Kontext der Flask-Anwendung verfügbar sind.

Test vor Implementierung (TDD-Prinzip)

Bevor nun die eigentliche Route implementiert wird, wird zunächst ein Test geschrieben.

Auf den ersten Blick wirkt es möglicherweise ungewöhnlich, etwas zu testen, das noch gar nicht existiert. Genau hier greift jedoch das konzeptionelle Modell des Test-Driven Development.

Die Anforderungen werden zuerst in einen Test überführt, der zunächst fehlschlägt. Anschließend wird die Implementierung schrittweise so lange ergänzt, bis der Test erfolgreich durchläuft. Dadurch wird sichergestellt, dass der Code exakt das erwartete Verhalten erfüllt.

Ein Minimaltest, der prüft, ob die Route existiert und Statuscode 200 liefert, könnte folgendermaßen aussehen:

def test_conduction_dummy_route(client):
    response = client.get("/conduction")

    assert response.status_code == 200

    data = response.get_json()
    assert data["status"] == "ok"
    assert "reachable" in data["message"]

Der Test nutzt dabei das zentrale pytest-Setup über conftest.py, welches einen vorkonfigurierten Flask-Testclient bereitstellt.

Erster Testlauf

Wird nun pytest ausgeführt, schlägt der Test erwartungsgemäß fehl:

FAILED tests/main/test_route.py::test_conduction_dummy_route - assert 404 == 200

Der Server liefert einen 404-Statuscode, da die Route noch nicht existiert. Dieses Verhalten ist korrekt und bestätigt, dass der Test sauber formuliert ist.

Erste Implementierung der Route

Nun wird eine minimale Implementierung ergänzt:

@bp.route("/conduction", methods=["GET"])
def conduction_dummy():
    return jsonify(status="ok", message="Dummy message"), 200

Ein erneuter Testlauf schlägt wieder fehl, zeigt nun aber ein anderes Ergebnis. Die Route ist erreichbar und liefert Statuscode 200, allerdings stimmt der erwartete Inhalt der Message noch nicht.

FAILED tests/main/test_route.py::test_conduction_dummy_route - AssertionError: assert 'reachable' in 'Dummy message'

Anpassung der Response

Nach Anpassung der Rückgabemeldung auf

message="Conduction endpoint reachable"

läuft der Test erfolgreich durch.

tests/main/test_route.py .  [100%]
1 passed in 0.04s

Zwischenfazit

Damit ist der erste Teil abgeschlossen. Das Beispiel ist bewusst minimal gehalten, zeigt jedoch bereits, dass:

  • die Route erreichbar ist
  • der Blueprint korrekt eingebunden ist
  • die Flask-Anwendung läuft
  • das Testsetup funktioniert

und liefert damit den ersten vollständigen technischen Durchlauf:

Client → Route → Blueprint → Response → Test

Gleichzeitig demonstriert dieser Schritt das grundlegende Prinzip einer testgetriebenen, iterativen Implementierung.

Test schreiben
↓
Test starten
↓
Fehlschlag
↓
Erste Implementierung
↓
Test starten
↓
Fehlschlag (Detail)
↓
Anpassung
↓
Test erfolgreich

Welchen Mehrwert diese Vorgehensweise insbesondere bei der Umsetzung der fachlichen Berechnungslogik bietet, wird im nächsten Abschnitt sichtbar.

Teil 2 - Erste Berechnung läuft

Nach dem im ersten Teil gezeigt wurde, dass die Projektstruktur funktional zusammenspielt und die Architektur korrekt verankert ist, geht es nun um die fachliche Umsetzung.

Im Mittelpunkt steht dabei das Service-Modul, in dem die Domänenlogik implementiert wird. Ergänzend wird eine JSON-Route als technische Adaption umgesetzt, die die Service-Logik von außen erreichbar macht, ohne bereits eine vollständige API darzustellen.

In diesem Schritt werden daher vier Dinge umgesetzt:

  • Tests für die Domänenlogik
  • Implementierung der Berechnungsfunktion
  • Tests für die Route
  • Dünne Integration über /conduction

Ziel ist der Nachweis, dass:

  • die fachliche Kernberechnung korrekt funktioniert
  • Grenzfälle und Fehlerfälle sauber behandelt werden
  • die Orchestrierung Route → Service arbeitet

Service-Modul und Funktionsgerüst

Zunächst wird im Service-Modul app/main/service.py eine leere Funktion angelegt:

def heat_flow_stationary_1d(
    thickness: float, area: float, conductivity: float, t_1: float, t_2: float
):
    pass

Diese Funktion bildet den Einstiegspunkt für die fachliche Berechnung.

Erwartungswerte und Tests für die Domänenlogik

Auch hier wird wieder testgetrieben gearbeitet. Im Unterschied zum ersten Teil liegt der Fokus nun jedoch nicht mehr auf der technischen Integration, sondern auf der fachlichen Modellierung.

Bevor Tests geschrieben werden können, muss zunächst die physikalische Logik betrachtet werden:

\[ \dot{Q} = \frac{\lambda \cdot A \cdot (T_1 - T_2)}{d} \]

Aus dieser Formel ergeben sich direkte physikalische Randbedingungen:

  • Wanddicke \(d\) muss \(\gt 0\) sein
  • Fläche \(A\) muss \(\ge 0\) sein
  • Wärmeleitfähigkeit \(\lambda\) muss \(\ge 0\) sein
  • Temperaturen besitzen keine Einschränkung

Zusätzlich dürfen keine Parameter None sein.

Aus diesen Anforderungen lassen sich die Tests für

  • korrekte Berechnung
  • physikalisch ungültige Parameter
  • fehlende Werte

direkt ableiten.


@pytest.mark.parametrize(
    "thickness, area, conductivity, t_1, t_2, expected",
    [
        (0.1, 1, 1, 10, 5, 50),
        (0.1, 1, 1, 5, 10, -50),
        (0.1, 2, 1, 10, 5, 100),
        (0.1, 1, 1, 10, 10, 0),
        (0.1, 0, 1, 10, 5, 0),
        (0.1, 1, 0, 10, 5, 0),
    ],
)
def test_heat_flow_stationary_1d_valid_cases(
    thickness, area, conductivity, t_1, t_2, expected
):
    assert heat_flow_stationary_1d(
        thickness, area, conductivity, t_1, t_2
    ) == pytest.approx(expected)


@pytest.mark.parametrize(
    "thickness, area, conductivity, error, message",
    [
        (0, 1, 1, ValueError, "thickness must be greater 0"),
        (-0.1, 1, 1, ValueError, "thickness must be greater 0"),
        (0.1, -1, 1, ValueError, "area must be equal or greater 0"),
        (0.1, 1, -1, ValueError, "conductivity must be equal or greater 0"),
    ],
)
def test_heat_flow_stationary_1d_invalid_parameters(
    thickness, area, conductivity, error, message
):
    with pytest.raises(error, match=message):
        heat_flow_stationary_1d(thickness, area, conductivity, 10, 5)


@pytest.mark.parametrize(
    "thickness, area, conductivity,t_1, t_2, error, message",
    [
        (None, 1, 1, 10, 5, ValueError, "input parameter must not be null"),
        (0.1, None, 1, 10, 5, ValueError, "input parameter must not be null"),
        (0.1, 1, None, 10, 5, ValueError, "input parameter must not be null"),
        (0.1, 1, 1, None, 5, ValueError, "input parameter must not be null"),
        (0.1, 1, 1, 10, None, ValueError, "input parameter must not be null"),
    ],
)
def test_heat_flow_stationary_1d_none_values(
    thickness, area, conductivity, t_1, t_2, error, message
):
    with pytest.raises(error, match=message):
        heat_flow_stationary_1d(thickness, area, conductivity, t_1, t_2)

Da aktuell nur eine leere Funktion existiert, schlagen erwartungsgemäß zunächst alle Tests fehl. Dies ist der gewünschte Ausgangspunkt, die Tests definieren das erwartete Verhalten, bevor Implementierungsdetails festgelegt werden.

Implementierung der Berechnungsfunktion

Die Implementierung erfolgt in zwei Schritten. Zunächst wird ausschließlich die mathematische Formel umgesetzt:

return conductivity * area * (t_1 - t_2) / thickness

Ein erneuter Testlauf zeigt nun, dass zwar korrekte Werte berechnet werden, jedoch weiterhin Fehler bei ungültigen Eingaben auftreten. Dies ist erwartbar, da bislang keine explizite Fehlerbehandlung implementiert wurde.

Fehlerbehandlung innerhalb der Methode

Basierend auf den zuvor definierten physikalischen Randbedingungen wird nun eine explizite Validierung in der Berechnungsfunktion ergänzt:

if any(v is None for v in (thickness, area, conductivity, t_1, t_2)):
    raise ValueError("input parameter must not be null")

if thickness <= 0:
    raise ValueError("thickness must be greater 0")
if area < 0:
    raise ValueError("area must be equal or greater 0")
if conductivity < 0:
    raise ValueError("conductivity must be equal or greater 0")

Nach dieser Implementierung laufen alle Service-Tests erfolgreich durch.

tests/main/test_service.py ............... [100%]

Damit ist sichergestellt:

  • korrekte mathematische Berechnung
  • definierte Fehlerfälle
  • klare fachliche Constraints

Dünne Integration über \conduction

Um die Service-Logik von aussen nutzbar zu machen, wird nun die Dummy-Route durch eine POST-Route ersetzt. Wichtig ist dabei die klare Verantwortungsaufteilung:

  • Service → prüft Fachlogik und wirft fachliche Exceptions
  • Route → verarbeitet HTTP-Request und mappt Exceptions auf Statuscodes

Für die Route werden daher eigene Tests definiert (test_routes.py). Diese prüfen nicht erneut die Mathematik, sondern ausschließlich:

  • erfolgreiche Verarbeitung gültiger Requests
  • Mapping von Fehlern auf HTTP-Statuscodes
  • Umgang mit unvollständigen JSON-Daten

Nach Umsetzung der Route laufen auch diese Tests erfolgreich durch.

Zwischenfazit

Damit ist auch der zweite Teil dieses Meilensteins abgeschlossen. Wie bereits im ersten Abschnitt stehen die Tests im Zentrum der Umsetzung. Der wesentliche Aufwand liegt dabei in der präzisen Definition der erwarteten fachlichen und technischen Verhalten, während die eigentliche Implementierung vergleichsweise kompakt bleibt.

Für diesen Schritt gilt daher bewusst die Regel:

Service prüft Mathematik und wirft fachliche Exceptions
Route prüft Kommunikation und übersetzt Exceptions in HTTP-Antworten

Diese Trennung stellt sicher, dass:

  • die Domänenlogik unabhängig vom Webframework bleibt
  • die HTTP-Schicht dünn bleibt
  • Tests gezielt auf unterschiedliche Verantwortungsebenen fokussieren

Testabdeckung

Bei konsequenter Test-First-Vorgehensweise ergibt sich automatisch eine hohe Testabdeckung. Mit pytest-cov ergibt sich:

Name                     Stmts   Miss  Cover   Missing
------------------------------------------------------
...
app/main/routes.py          15      0   100%
app/main/service.py         10      0   100%

Für die Domänenlogik ist eine vollständige Abdeckung sinnvoll, da hier fachliche Kernfunktionalität liegt. Für Routing-Code gelten bereits Werte um 80-90% als sehr gut.

Fazit und Ausblick

Ziel dieses Meilensteins war eine erste lauffähige Berechnung umzusetzen, zunächst noch ohne Benutzeroberfläche oder vollständiger REST-API.

Im ersten Schritt wurde die Anwendung strukturell vollständig verbunden und überprüft, ob Projektstruktur, Blueprint-Registrierung und Routing korrekt zusammenspielen. Die Dummy-Route diente dabei bewusst als minimaler technischer Nachweis, dass die Anwendung erreichbar ist und die grundlegende Architektur funktioniert.

Darauf aufbauend folgte im zweiten Schritt die Implementierung der eigentlichen Berechnungsfunktion. Hier stand die testgetriebene Entwicklung im Mittelpunkt. Zunächst wurden fachliche Erwartungswerte und physikalische Randbedingungen definiert und in Tests überführt, anschließend erfolgte die Implementierung, teilweise iterativ, so lange, bis das geforderte Verhalten reproduzierbar erfühlt war.

Tests fungieren in diesem Meilenstein bewusst als technischer Proof-of-Work sowohl für die Architektur als auch für die korrekte Umsetzung der Domänenlogik. Dabei prüfen Service-Tests gezielt die mathematische Fachlogik, während Route-Tests die Kommunikationsebene und das HTTP-Fehlermapping absichern.

Damit ist ein zentraler Entwicklungsschritt erreicht:

Das Herzstück der Anwendung funktioniert, die Berechnung läuft und liefert reproduzierbare Ergebnisse.

Auch wenn nach außen aktuell nur eine dünne technische Integration existiert, steht nun eine saubere, getestete Berechnungsmethode mit klar definiertem Fehlerhandling als stabile Grundlage zur Verfügung.

Im nächsten Meilenstein Von der Berechnung zur ersten interaktiven App verschiebt sich der Fokus erstmals auf die Benutzeroberfläche. Mit einer einfachen statischen UI im Sinne einer serverseitig gerenderten Anwendung erhält das Projekt dann zum ersten Mal ein sichtbares Interface, und damit ein ersten “Gesicht”.

Feedback, Fragen oder Anregungen sind jederzeit willkommen, gerne per Email oder auch auf LinkedIn.