Vier gängige Arten der Codeabdeckung

Erfahren Sie, was Codeabdeckung ist, und lernen Sie vier gängige Methoden zu ihrer Messung kennen.

Haben Sie schon einmal den Begriff „Codeabdeckung“ gehört? In diesem Post beschäftigen wir uns mit der Codeabdeckung in Tests und mit vier gängigen Methoden zu ihrer Messung.

Was ist Codeabdeckung?

Die Codeabdeckung ist ein Messwert, der den Prozentsatz des Quellcodes misst, den Ihre Tests ausführen. So können Sie Bereiche identifizieren, in denen möglicherweise keine ordnungsgemäßen Tests durchgeführt wurden.

Häufig werden diese Messwerte so aufgezeichnet:

Datei Aussagen (%) Verzweigung (%) %-Funktionen Linien (%) Nicht erkannte Linien
file.js 90 % 100 % 90 % 80 % 89.256
coffee.js 55,55% 80 % 50 % 62,5% 10–11, 18

Wenn Sie neue Funktionen und Tests hinzufügen, können Sie mit zunehmender Codeabdeckung die Gewissheit gewinnen, dass Ihre Anwendung gründlich getestet wurde. Es gibt jedoch noch mehr zu entdecken.

Vier gängige Arten der Codeabdeckung

Es gibt vier gängige Möglichkeiten, die Codeabdeckung zu erfassen und zu berechnen: Funktions-, Zeilen-, Zweig- und Anweisungsabdeckung.

Vier Arten der Textabdeckung.

Um zu sehen, wie jeder Typ der Codeabdeckung seinen Prozentsatz berechnet, sehen Sie sich das folgende Codebeispiel für die Berechnung von Kaffeezutaten an:

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  return {};
}

export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}

Die calcCoffeeIngredient-Funktion wird mit folgenden Tests geprüft:

/* coffee.test.js */

import { describe, expect, assert, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-incomplete';

describe('Coffee', () => {
  it('should have espresso', () => {
    const result = calcCoffeeIngredient('espresso', 2);
    expect(result).to.deep.equal({ espresso: 60 });
  });

  it('should have nothing', () => {
    const result = calcCoffeeIngredient('unknown');
    expect(result).to.deep.equal({});
  });
});

Sie können den Code und die Tests in dieser Live-Demo ausführen oder sich das Repository ansehen.

Funktionsabdeckung

Codeabdeckung: 50%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  // ...
}

function isValidCoffee(name) {
  // ...
}

Die Funktionsabdeckung ist ein einfacher Messwert. Damit wird der Prozentsatz der Funktionen in Ihrem Code erfasst, den Ihre Tests aufrufen.

Im Codebeispiel gibt es zwei Funktionen: calcCoffeeIngredient und isValidCoffee. In den Tests wird nur die Funktion calcCoffeeIngredient aufgerufen, sodass die Funktionsabdeckung 50 % beträgt.

Linienabdeckung

Codeabdeckung: 62,5%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  return {};
}

export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}

Mit der Zeilenabdeckung wird der Prozentsatz der ausführbaren Codezeilen gemessen, die von der Testsuite ausgeführt wurden. Wenn eine Codezeile nicht ausgeführt wird, bedeutet dies, dass ein Teil des Codes nicht getestet wurde.

Das Codebeispiel enthält acht Zeilen ausführbaren Codes (rot und grün hervorgehoben), aber die Tests führen die Bedingung americano (zwei Zeilen) und die Funktion isValidCoffee (eine Zeile) nicht aus. Daraus ergibt sich eine Linienabdeckung von 62,5%.

Bei der Zeilenabdeckung werden keine Deklarationen wie function isValidCoffee(name) und let espresso, water; berücksichtigt, da diese nicht ausführbar sind.

Filialabdeckung

Codeabdeckung: 80%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  // ...

  if (coffeeName === 'espresso') {
    // ...
    return { espresso };
  }

  if (coffeeName === 'americano') {
    // ...
    return { espresso, water };
  }

  return {};
}
…

Die Zweigabdeckung misst den Prozentsatz der ausgeführten Zweige oder Entscheidungspunkte im Code, z. B. if-Anweisungen oder Schleifen. Sie bestimmt, ob Tests sowohl die wahren als auch die falschen Zweige von bedingten Anweisungen untersuchen.

Im Codebeispiel gibt es fünf Zweige:

  1. calcCoffeeIngredient wird mit nur coffeeName Chek-Zeichen. angerufen
  2. calcCoffeeIngredient mit coffeeName und cup Chek-Zeichen. wird angerufen
  3. Kaffee ist Espresso Chek-Zeichen.
  4. Kaffee ist Americano X-Zeichen.
  5. Sonstiger Kaffee Chek-Zeichen.

Die Tests decken alle Branches mit Ausnahme der Bedingung Coffee is Americano ab. Die Filialabdeckung beträgt also 80%.

Abdeckung des Kontoauszugs

Codeabdeckung: 55,55%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  return {};
}

export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}

Die Anweisungsabdeckung misst den Prozentsatz der Anweisungen im Code, der von den Tests ausgeführt wird. Auf den ersten Blick fragen Sie sich vielleicht: „Ist das nicht dasselbe wie die Netzabdeckung?“ Tatsächlich ähnelt die Anweisungsabdeckung der Zeilenabdeckung, wobei einzelne Codezeilen berücksichtigt werden, die mehrere Anweisungen enthalten.

Im Codebeispiel gibt es acht Zeilen ausführbaren Codes, aber es gibt neun Anweisungen. Sehen Sie die Zeile mit zwei Anweisungen?

Antwort prüfen

Sie lautet: espresso = 30 * cup; water = 70 * cup;

Die Tests decken nur fünf der neun Aussagen ab, sodass die Aussageabdeckung 55,55 % beträgt.

Wenn Sie immer eine Aussage pro Zeile schreiben, entspricht die Zeilenabdeckung der des Kontoauszugs.

Welche Art von Codeabdeckung sollten Sie auswählen?

Die meisten Tools zur Codeabdeckung umfassen diese vier Arten der gängigen Codeabdeckung. Die Wahl der zu priorisierenden Codeabdeckungsmetrik hängt von den spezifischen Projektanforderungen, Entwicklungspraktiken und Testzielen ab.

Im Allgemeinen ist die Abdeckung von Kontoauszügen ein guter Ausgangspunkt, da es sich um einen einfachen und leicht verständlichen Messwert handelt. Im Gegensatz zur Anweisungsabdeckung misst die Zweigabdeckung und die Funktionsabdeckung, ob Tests eine Bedingung (Zweig) oder eine Funktion aufrufen. Daher sind sie eine natürliche Abfolge nach der Abdeckung der Anweisungen.

Sobald Sie eine hohe Anweisungsabdeckung erreicht haben, können Sie mit der Zweigabdeckung und der Funktionsabdeckung fortfahren.

Entspricht die Testabdeckung der Codeabdeckung?

Nein. Testabdeckung und Codeabdeckung sind oft verwechselt, unterscheiden sich aber voneinander:

  • Testabdeckung: Aqualitativer Messwert, der angibt, wie gut die Testsuite die Funktionen der Software abdeckt. Sie hilft bei der Bestimmung des Risikoniveaus.
  • Codeabdeckung: Ein quantitativer Messwert, mit dem der Anteil des Codes gemessen wird, der während des Tests ausgeführt wird. Es geht darum, wie viel Code die Tests abdecken.

Hier ist eine vereinfachte Analogie: Stellen Sie sich eine Webanwendung wie ein Haus vor.

  • Mit der Testabdeckung wird gemessen, wie gut die Tests die Räume im Zuhause abdecken.
  • Die Codeabdeckung misst, wie viel des Hauses die Tests durchlaufen haben.

Eine Codeabdeckung von 100% bedeutet nicht, dass es keine Fehler gibt.

Es ist zwar durchaus wünschenswert, beim Testen eine hohe Codeabdeckung zu erzielen, eine 100% ige Codeabdeckung garantiert jedoch nicht das Fehlen von Fehlern oder Schwachstellen in Ihrem Code.

Eine bedeutungslose Möglichkeit, eine Codeabdeckung von 100% zu erreichen

Sehen Sie sich den folgenden Test an:

/* coffee.test.js */

// ...
describe('Warning: Do not do this', () => {
  it('is meaningless', () => { 
    calcCoffeeIngredient('espresso', 2);
    calcCoffeeIngredient('americano');
    calcCoffeeIngredient('unknown');
    isValidCoffee('mocha');
    expect(true).toBe(true); // not meaningful assertion
  });
});

Dieser Test erreicht 100% Funktions-, Zeilen-, Zweig- und Anweisungsabdeckung, ergibt jedoch keinen Sinn, da der Code nicht tatsächlich getestet wird. Die Assertion expect(true).toBe(true) ist immer erfolgreich, unabhängig davon, ob der Code korrekt funktioniert.

Ein schlechter Messwert ist schlechter als kein Messwert

Ein schlechter Messwert kann ein falsches Sicherheitsgefühl vermitteln, was noch schlimmer ist, als gar keine Messwerte zu haben. Wenn Sie beispielsweise eine Testsuite haben, die eine Codeabdeckung von 100% erreicht, die Tests jedoch alle bedeutungslos sind, haben Sie möglicherweise ein falsches Sicherheitsgefühl, dass Ihr Code gut getestet wurde. Wenn Sie einen Teil des Anwendungscodes versehentlich löschen oder kaputt machen, sind die Tests weiterhin erfolgreich, auch wenn die Anwendung nicht mehr ordnungsgemäß funktioniert.

So vermeiden Sie dieses Szenario:

  • Testprüfung. Schreiben und überprüfen Sie Tests, um sicherzustellen, dass sie aussagekräftig sind, und testen Sie den Code in einer Vielzahl verschiedener Szenarien.
  • Verwenden Sie die Codeabdeckung als Richtlinie und nicht als einziges Maß für die Wirksamkeit oder die Codequalität von Tests.

Codeabdeckung in verschiedenen Testtypen verwenden

Sehen wir uns genauer an, wie Sie die Codeabdeckung mit den drei gängigen Testtypen verwenden können:

  • Unittests. Sie eignen sich am besten zum Erfassen der Codeabdeckung, da sie mehrere kleine Szenarien und Testpfade abdecken sollen.
  • Integrationstests. Sie können bei der Erfassung der Codeabdeckung für Integrationstests helfen, verwenden sie jedoch mit Vorsicht. In diesem Fall berechnen Sie die Abdeckung eines größeren Teils des Quellcodes, und es kann schwierig sein zu bestimmen, welche Tests tatsächlich welche Teile des Codes abdecken. Trotzdem kann die Berechnung der Codeabdeckung von Integrationstests für Legacy-Systeme nützlich sein, die keine gut isolierten Einheiten haben.
  • End-to-End-Tests (E2E): Das Messen der Codeabdeckung für E2E-Tests ist aufgrund der komplexen Natur dieser Tests schwierig und herausfordernd. Statt eine Codeabdeckung zu verwenden, ist die Abdeckung von Anforderungen möglicherweise die bessere Wahl. Das liegt daran, dass der Schwerpunkt bei E2E-Tests auf den Anforderungen des Tests liegt und nicht auf dem Quellcode.

Fazit

Die Codeabdeckung kann ein nützlicher Messwert sein, um die Effektivität Ihrer Tests zu messen. Sie können damit die Qualität Ihrer Anwendung verbessern, indem sichergestellt wird, dass die entscheidende Logik in Ihrem Code gut getestet wird.

Die Codeabdeckung ist jedoch nur ein Messwert. Berücksichtigen Sie auch andere Faktoren wie die Qualität Ihrer Tests und die Anforderungen Ihrer Anwendung.

Eine Codeabdeckung von 100% ist nicht das Ziel. Stattdessen sollten Sie die Codeabdeckung in Verbindung mit einem ausgefeilten Testplan verwenden, der eine Vielzahl von Testmethoden umfasst, darunter Unit-, Integrations-, End-to-End- und manuelle Tests.

Sehen Sie sich das vollständige Codebeispiel und Tests mit guter Codeabdeckung an. Sie können den Code und die Tests auch in dieser Live-Demo ausführen.

/* coffee.js - a complete example */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  if (!isValidCoffee(coffeeName)) return {};

  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  throw new Error (`${coffeeName} not found`);
}

function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}
/* coffee.test.js - a complete test suite */

import { describe, expect, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-complete';

describe('Coffee', () => {
  it('should have espresso', () => {
    const result = calcCoffeeIngredient('espresso', 2);
    expect(result).to.deep.equal({ espresso: 60 });
  });

  it('should have americano', () => {
    const result = calcCoffeeIngredient('americano');
    expect(result.espresso).to.equal(30);
    expect(result.water).to.equal(70);
  });

  it('should throw error', () => {
    const func = () => calcCoffeeIngredient('mocha');
    expect(func).toThrowError(new Error('mocha not found'));
  });

  it('should have nothing', () => {
    const result = calcCoffeeIngredient('unknown')
    expect(result).to.deep.equal({});
  });
});