Code Coverage (Testabdeckung) ist ein Maß dafür, wie viel des Anwendungscodes durch automatisierte Tests ausgeführt wird. In Angular-Projekten ist Code Coverage ein wichtiges Instrument zur Beurteilung der Testqualität, jedoch mit entscheidenden Einschränkungen, die jeder Entwickler verstehen sollte.
Code Coverage misst in der Regel vier Hauptaspekte:
In Angular-Projekten ist die Messung der Code Coverage bereits in das Build-System integriert:
# Test mit Coverage-Bericht ausführen
ng test --code-coverage
# Alternative für Jest
ng test --coverageDies erzeugt einen Coverage-Bericht im
/coverage-Verzeichnis des Projekts, der detaillierte
Informationen über die Testabdeckung liefert und als HTML-Seite geöffnet
werden kann.
In der Datei angular.json können Mindestanforderungen
für die Code Coverage festgelegt werden:
"test": {
"options": {
"codeCoverage": true,
"codeCoverageExclude": [
"src/testing/**/*",
"src/environments/**/*"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 85,
"lines": 90,
"statements": 90
}
}
}
}Mit Tools wie karma-coverage kann der Testlauf
fehlschlagen, wenn die definierten Schwellenwerte nicht erreicht
werden.
Ein hoher Coverage-Wert, selbst 100%, garantiert keineswegs fehlerfreien Code. Diese Erkenntnis ist entscheidend für die richtige Einschätzung von Testabdeckungsmetriken.
Ein Testabdeckungswert von 100% bedeutet lediglich, dass jede Codezeile während der Tests mindestens einmal ausgeführt wurde. Dies sagt jedoch nichts darüber aus:
function verifyAge(age: number): string {
if (age < 0) {
return 'Invalid age';
} else if (age < 18) {
return 'Minor';
} else if (age < 65) {
return 'Adult';
} else {
return 'Senior';
}
}Ein Test, der nur die Werte -1, 10, 30 und 70 prüft, würde 100% Code Coverage erreichen, aber zahlreiche kritische Grenzfälle auslassen, wie z.B. den exakten Wert 18 oder 65.
function processUserData(user: User): UserStats {
const stats: UserStats = { activityLevel: 'low' };
if (user.activities && user.activities.length > 0) {
let totalPoints = 0;
for (const activity of user.activities) {
totalPoints += activity.points || 0;
}
if (totalPoints > 1000) {
stats.activityLevel = 'high';
} else if (totalPoints > 100) {
stats.activityLevel = 'medium';
}
}
return stats;
}Selbst bei 100% Coverage könnten wichtige Szenarien ungetestet
bleiben, wie der Fall, dass activity.points undefined ist,
oder der Grenzfall von genau 100 oder 1000 Punkten.
Ein hohes Coverage-Maß kann ein falsches Sicherheitsgefühl erzeugen. Entwicklerteams können in die Falle tappen, Tests zu schreiben, die primär auf die Erhöhung der Coverage-Werte abzielen, anstatt auf das Testen funktionaler Anforderungen und Randbedingungen.
Um die Grenzen der reinen Coverage-Messung zu überwinden, ist das Testen von Grenzwerten (Boundary Testing) entscheidend.
Für jeden entscheidenden Bedingungspunkt sollten wir Testfälle für:
describe('verifyAge function', () => {
// Ungültige Eingaben
it('should identify negative ages as invalid', () => {
expect(verifyAge(-1)).toBe('Invalid age');
expect(verifyAge(-100)).toBe('Invalid age');
});
// Minderjährige - Grenztests
it('should identify ages 0 to 17 as minor', () => {
expect(verifyAge(0)).toBe('Minor'); // Untergrenze
expect(verifyAge(10)).toBe('Minor'); // Mittlerer Wert
expect(verifyAge(17)).toBe('Minor'); // Knapp unter der Grenze
});
// Erwachsene - Grenztests
it('should identify ages 18 to 64 as adult', () => {
expect(verifyAge(18)).toBe('Adult'); // Exakt auf der Grenze
expect(verifyAge(19)).toBe('Adult'); // Knapp über der Grenze
expect(verifyAge(40)).toBe('Adult'); // Mittlerer Wert
expect(verifyAge(64)).toBe('Adult'); // Knapp unter der nächsten Grenze
});
// Senioren - Grenztests
it('should identify ages 65 and above as senior', () => {
expect(verifyAge(65)).toBe('Senior'); // Exakt auf der Grenze
expect(verifyAge(66)).toBe('Senior'); // Knapp über der Grenze
expect(verifyAge(90)).toBe('Senior'); // Höherer Wert
});
});Statt blind auf 100% Coverage zu zielen, sollten Entwickler Äquivalenzklassen identifizieren – Eingabebereiche, die voraussichtlich das gleiche Verhalten auslösen – und dann Grenzwerte für jede Klasse testen.
Für eine Funktion, die Rabatte basierend auf Bestellwerten berechnet:
describe('calculateDiscount function', () => {
// Äquivalenzklasse: Kein Rabatt (0-99€)
it('should apply no discount for orders below 100€', () => {
expect(calculateDiscount(0)).toBe(0); // Untergrenze
expect(calculateDiscount(50)).toBe(0); // Mittlerer Wert
expect(calculateDiscount(99.99)).toBe(0); // Knapp unter der Grenze
});
// Äquivalenzklasse: 5% Rabatt (100-499€)
it('should apply 5% discount for orders between 100€ and 499€', () => {
expect(calculateDiscount(100)).toBe(5); // Exakt auf der Grenze
expect(calculateDiscount(100.01)).toBe(5); // Knapp über der Grenze
expect(calculateDiscount(300)).toBe(5); // Mittlerer Wert
expect(calculateDiscount(499.99)).toBe(5); // Knapp unter der nächsten Grenze
});
// Äquivalenzklasse: 10% Rabatt (500€+)
it('should apply 10% discount for orders of 500€ or more', () => {
expect(calculateDiscount(500)).toBe(10); // Exakt auf der Grenze
expect(calculateDiscount(500.01)).toBe(10);// Knapp über der Grenze
expect(calculateDiscount(1000)).toBe(10); // Höherer Wert
});
});Code Coverage ist besonders wertvoll, wenn sie in Continuous Integration eingebunden wird:
# .gitlab-ci.yml Beispiel
test:
stage: test
script:
- ng test --code-coverage --watch=false
coverage: /Statements\s+:\s+(\d+\.?\d*)%/
artifacts:
paths:
- coverage/Das ermöglicht die Überwachung der Coverage-Entwicklung im Zeitverlauf und kann bei Unterschreitung definierter Schwellenwerte automatisch Warnungen oder Fehler auslösen.
Ein realistischer und wertvoller Ansatz zur Code Coverage sollte Folgendes berücksichtigen:
Coverage als Werkzeug, nicht als Ziel: Verwenden Sie Coverage-Berichte, um ungetestete Bereiche zu identifizieren, nicht als primäres Erfolgskriterium.
Funktionale Abdeckung: Messen Sie, welcher Prozentsatz der funktionalen Anforderungen durch Tests abgedeckt ist, nicht nur Code-Zeilen.
Risikoorientiertes Testen: Widmen Sie kritischen oder komplexen Komponenten mehr Testaufwand als einfachem, unkritischem Code.
Realistische Schwellenwerte: Setzen Sie praktikable Coverage-Schwellenwerte basierend auf Projektkontext und Ressourcen.
Qualität vor Quantität: Ein einzelner durchdachter Test kann wertvoller sein als viele oberflächliche.
Code Coverage ist ein nützliches Werkzeug, aber nur ein Element einer umfassenden Teststrategie. Ein ausgewogener Ansatz kombiniert Coverage-Metriken mit:
Die Qualität eines Test-Suites wird letztendlich nicht durch einen Prozentwert definiert, sondern durch seine Fähigkeit, Fehler zu finden, Regressionsprobleme zu verhindern und das Vertrauen in die Anwendung zu stärken.
// Ein einfaches Beispiel, das die Grenzen von Coverage-Metriken demonstriert:
function divideNumbers(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
// Ein Test mit 100% Coverage, der aber wichtige Fälle missachtet
it('should divide numbers', () => {
expect(divideNumbers(10, 2)).toBe(5);
});
// Besser wäre:
describe('divideNumbers', () => {
it('should correctly divide positive numbers', () => {
expect(divideNumbers(10, 2)).toBe(5);
});
it('should correctly divide with negative numbers', () => {
expect(divideNumbers(-10, 2)).toBe(-5);
expect(divideNumbers(10, -2)).toBe(-5);
expect(divideNumbers(-10, -2)).toBe(5);
});
it('should correctly handle division by decimals', () => {
expect(divideNumbers(10, 0.5)).toBe(20);
});
it('should throw error when dividing by zero', () => {
expect(() => divideNumbers(10, 0)).toThrowError('Division by zero');
});
});