Die letzte und schwerste Frage. Zu wenige Tests lassen Fehler durch; die falschen Tests machen jede Änderung teuer, ohne einen einzigen Fehler zu verhindern.
Verhalten testen, nicht Implementierung
Der teuerste Testfehler ist der Test, der weiss, wie etwas gemacht wird, statt was dabei herauskommt:
Prüft die Implementierung
[Fact]
public void Speichert_RuftRepositoryAuf()
{
service.Speichere(kunde);
_repo.Verify(r =>
r.FuegeHinzu(
It.IsAny<Kunde>()),
Times.Once);
_cache.Verify(c =>
c.Leere(), Times.Once);
}Prüft das Verhalten
[Fact]
public void Speichern_MachtKundenAuffindbar()
{
service.Speichere(kunde);
var gefunden =
service.Finde(kunde.Id);
Assert.Equal(kunde.Name,
gefunden.Name);
}Links bricht der Test, sobald jemand die interne Reihenfolge ändert — obwohl das Verhalten identisch bleibt. Rechts bricht er nur, wenn tatsächlich etwas kaputt ist.
Woran du diese Tests erkennst
Sie brechen bei jedem Refactoring, obwohl sich fachlich nichts geändert hat. Das ist kein Zeichen guter Abdeckung, sondern das Gegenteil: Der Test hält die Implementierung fest, statt das Versprechen abzusichern — und macht damit genau die Änderungen teuer, die Tests eigentlich ermöglichen sollen.
Private Methoden nicht direkt testen
Der Wunsch kommt regelmässig auf, und die Antwort ist fast immer dieselbe: Eine private Methode wird durch die öffentliche getestet, die sie benutzt. Ist das zu umständlich, sagt das etwas aus:
- Die private Methode enthält Logik, die einen eigenen Typ verdient — dann zieh sie heraus, und sie ist öffentlich testbar.
- Oder die öffentliche Methode tut zu viel.
InternalsVisibleTo verschiebt die Frage nur; ein Test auf einer internen Methode ist genauso an die Implementierung gekoppelt wie ein Mock-Test.
Mocks sparsam
Ein Mock ist richtig für das, was du nicht kontrollierst: Netzwerk, Zeit, Zufall, fremde Systeme. Für alles andere ist das echte Objekt einfacher und ehrlicher.
// Unnötig: ein Mock für ein reines Rechenobjekt
var rechner = new Mock<IRabattRechner>();
rechner.Setup(r => r.Berechne(100m, 3)).Returns(95m);
// Einfacher, und prüft echtes Verhalten:
var rechner = new RabattRechner();Die Faustregel
Mocke, was langsam, unberechenbar oder fremd ist. Alles andere echt einsetzen. Ein Test mit fünf Mocks prüft meistens nur noch, dass deine Mocks so konfiguriert sind, wie du sie konfiguriert hast.
Die Testpyramide, kurz
- Unit-Tests — viele, in Millisekunden, für Logik und Randfälle.
- Integrationstests — deutlich weniger, für die Nahtstellen: Läuft die Abfrage wirklich gegen Postgres?
WebApplicationFactoryund Testcontainers machen das in .NET unaufwendig. - End-to-End — eine Handvoll, für die wichtigsten Abläufe. Sie sind langsam und brüchig; als Sicherheitsnetz gut, als Hauptwerkzeug ruinös.
Und die Abdeckungszahl
80% Testabdeckung sagt, dass 80% der Zeilen ausgeführt wurden. Nicht, dass sie geprüft wurden — ein Test ohne Assert erzeugt dieselbe Zahl.
[Fact]
public void Deckt100ProzentAb()
{
var rechner = new Rabatt();
rechner.Berechne(100m, 6); // durchlaufen — aber nichts geprüft
}Als Suchhilfe ist die Zahl nützlich: Ein Coverage-Bericht zeigt dir ungetestete Zweige, an die niemand gedacht hat. Als Zielvorgabe erzeugt sie Tests, die geschrieben werden, um eine Zahl zu erreichen — und die prüfen dann typischerweise Getter.
Die bessere Frage ist nicht «wie viel Prozent», sondern: Wenn ich diese Zeile falsch mache — wird ein Test rot?