Vier gängige Arten der Codeabdeckung

Hier erfahren Sie, was Codeabdeckung ist und wie Sie sie auf vier gängige Arten messen können.

Kennen Sie den Begriff „Code Coverage“? In diesem Beitrag erfahren Sie, was die Codeabdeckung in Tests ist und wie Sie sie mit vier gängigen Methoden messen können.

Was ist Codeabdeckung?

Die Codeabdeckung ist ein Messwert, mit dem der Prozentsatz des Quellcodes gemessen wird, der in Ihren Tests ausgeführt wird. So können Sie Bereiche identifizieren, die möglicherweise nicht ausreichend getestet wurden.

Häufig sieht die Erfassung dieser Messwerte so aus:

Datei %-Angaben % Branch Funktionen mit Prozentsatz % Zeilen Nicht abgedeckte 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 durch eine höhere Codeabdeckungsrate die Gewissheit haben, 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-, Verzweigungs- und Anweisungsabdeckung.

Vier Arten von Textabdeckung

Im folgenden Codebeispiel zur Berechnung der Kaffeezutaten sehen Sie, wie der Prozentsatz für die einzelnen Arten der Codeabdeckung berechnet wird:

/* 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 Tests zur Überprüfung der Funktion calcCoffeeIngredient sind:

/* 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 das Repository ansehen.

Funktionsabdeckung

Codeabdeckung: 50%

/* coffee.js */

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

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

Die Funktionsabdeckung ist ein einfacher Messwert. Er gibt den Prozentsatz der Funktionen in Ihrem Code an, die von Ihren Tests aufgerufen werden.

Im Codebeispiel gibt es zwei Funktionen: calcCoffeeIngredient und isValidCoffee. Die Tests rufen nur die Funktion calcCoffeeIngredient auf, 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 Ihrer Testsuite ausgeführt wurden. Wenn eine Codezeile nicht ausgeführt wird, bedeutet das, 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 americano-Bedingung (zwei Zeilen) und die isValidCoffee-Funktion (eine Zeile) nicht aus. Dies ergibt eine Linienabdeckung von 62,5%.

Bei der Zeilenabdeckung werden Deklarationsanweisungen wie function isValidCoffee(name) und let espresso, water; nicht berücksichtigt, da sie 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 Abschnittsabdeckung gibt den Prozentsatz der ausgeführten Verzweigungen oder Entscheidungspunkte im Code an, z. B. Wenn-Bedingungen oder Schleifen. Sie gibt an, ob in Tests sowohl der True- als auch der False-Zweig von bedingten Anweisungen geprüft wird.

Das Codebeispiel enthält fünf Verzweigungen:

  1. calcCoffeeIngredient nur mit coffeeName Häkchen anrufen
  2. calcCoffeeIngredient wird mit coffeeName und cup angerufen Häkchen
  3. Kaffee ist Espresso Häkchen
  4. Kaffee ist Americano X-Markierung
  5. Sonstige Kaffeesorten Häkchen

Die Tests decken alle Verzweigungen mit Ausnahme der Bedingung Coffee is Americano ab. Die Abdeckung der Filiale beträgt also 80%.

Abdeckung der Erklärung

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 gibt den Prozentsatz der Anweisungen in Ihrem Code an, die von Ihren Tests ausgeführt werden. Auf den ersten Blick könnten Sie sich fragen: „Ist das nicht dasselbe wie die Zeilenabdeckung?“ Tatsächlich ähnelt die Anweisungsabdeckung der Zeilenabdeckung, berücksichtigt aber einzelne Codezeilen, die mehrere Anweisungen enthalten.

Im Codebeispiel gibt es acht Zeilen ausführbaren Codes, aber neun Anweisungen. Können Sie die Zeile mit den zwei Anweisungen erkennen?

Antwort prüfen

Das ist die folgende Zeile: espresso = 30 * cup; water = 70 * cup;

Die Tests decken nur fünf der neun Aussagen ab.Die Abdeckung der Aussagen beträgt daher 55, 55%.

Wenn Sie immer eine Zeile pro Anweisung schreiben, entspricht die Zeilenabdeckung der Anweisungsabdeckung.

Welche Art von Codeabdeckung sollten Sie auswählen?

Die meisten Tools zur Codeabdeckung umfassen diese vier gängigen Arten der Codeabdeckung. Welchen Messwert für die Codeabdeckung Sie priorisieren, hängt von den spezifischen Projektanforderungen, Entwicklungspraktiken und Testzielen ab.

Im Allgemeinen ist die Aussageabdeckung ein guter Ausgangspunkt, da es sich um einen einfachen und leicht verständlichen Messwert handelt. Im Gegensatz zur Anweisungsabdeckung wird bei der Verzweigungsabdeckung und der Funktionsabdeckung gemessen, ob Tests eine Bedingung (Verzweigung) oder eine Funktion aufrufen. Daher sind sie eine natürliche Weiterentwicklung nach der Abdeckung von Anweisungen.

Sobald Sie eine hohe Anweisungsabdeckung erreicht haben, können Sie mit der Abdeckung von Verzweigungen und Funktionen fortfahren.

Ist die Testabdeckung dasselbe wie die Codeabdeckung?

Nein. Testabdeckung und Codeabdeckung werden oft verwechselt, sind aber unterschiedlich:

  • Testabdeckung: Ein qualitativer Messwert, der angibt, wie gut die Testsuite die Funktionen der Software abdeckt. Sie hilft, das damit verbundene Risiko zu bestimmen.
  • Codeabdeckung: Ein quantitativer Messwert, der den Anteil des während des Tests ausgeführten Codes misst. Es geht darum, wie viel Code die Tests abdecken.

Hier eine vereinfachte Analogie: Stellen Sie sich eine Webanwendung als Haus vor.

  • Mit der Testabdeckung wird gemessen, inwieweit die Tests die Räume im Zuhause abdecken.
  • Mit der Codeabdeckung wird gemessen, wie viel des Hauses die Tests abgedeckt haben.

100% Codeabdeckung bedeutet nicht, dass es keine Fehler gibt

Es ist zwar wünschenswert, bei Tests eine hohe Codeabdeckung zu erreichen, aber eine Codeabdeckung von 100% ist keine Garantie dafür, dass Ihr Code keine Fehler oder Mängel aufweist.

Eine sinnlose Methode, um eine Codeabdeckung von 100% zu erreichen

Betrachten Sie den folgenden Test:

/* 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 eine Abdeckung von 100% für Funktionen, Zeilen, Verzweigungen und Anweisungen, macht aber keinen Sinn, da der Code nicht wirklich getestet wird. Die expect(true).toBe(true)-Behauptung wird immer bestanden, unabhängig davon, ob der Code richtig funktioniert.

Ein schlechter Messwert ist schlimmer als gar kein Messwert

Ein schlechter Messwert kann ein falsches Sicherheitsgefühl vermitteln, was schlimmer ist, als gar keinen Messwert zu haben. Wenn Sie beispielsweise eine Testsuite haben, die eine Codeabdeckung von 100% erreicht, die Tests aber alle sinnlos sind, können Sie sich in Sicherheit wähnen, dass Ihr Code gut getestet ist. Wenn Sie versehentlich einen Teil des Anwendungscodes löschen oder beschädigen, werden die Tests trotzdem bestanden, auch wenn die Anwendung nicht mehr richtig funktioniert.

So vermeiden Sie dieses Szenario:

  • Testüberprü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 Anhaltspunkt, nicht als einziges Maß für die Testeffektivität oder Codequalität.

Codeabdeckung bei verschiedenen Arten von Tests verwenden

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

  • Unittests. Sie sind der beste Testtyp für die Erfassung der Codeabdeckung, da sie mehrere kleine Szenarien und Testpfade abdecken.
  • Integrationstests. Sie können dabei helfen, die Codeabdeckung für Integrationstests zu erfassen, sollten aber mit Vorsicht verwendet werden. In diesem Fall berechnen Sie die Abdeckung eines größeren Teils des Quellcodes. Es kann schwierig sein, zu ermitteln, welche Tests tatsächlich welche Teile des Codes abdecken. Dennoch kann die Berechnung der Codeabdeckung von Integrationstests für Altsysteme nützlich sein, die keine gut isolierten Einheiten haben.
  • End-to-End-Tests (E2E) Die Codeabdeckung für End-to-End-Tests zu messen, ist aufgrund der Komplexität dieser Tests schwierig und herausfordernd. Anstatt die Codeabdeckung zu verwenden, ist die Abdeckung der Anforderungen möglicherweise die bessere Lösung. Das liegt daran, dass bei End-to-End-Tests der Schwerpunkt auf der Abdeckung der Anforderungen Ihres Tests liegt und nicht auf dem Quellcode.

Fazit

Die Codeabdeckung kann ein nützlicher Messwert für die Effektivität Ihrer Tests sein. Sie können die Qualität Ihrer Anwendung verbessern, indem Sie dafür sorgen, dass die wichtige Logik in Ihrem Code gut getestet wird.

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

Es geht nicht darum, eine Codeabdeckung von 100% anzustreben. Stattdessen sollten Sie die Codeabdeckung in Kombination mit einem ausgewogenen Testplan verwenden, der eine Vielzahl von Testmethoden umfasst, darunter Unit-, Integrations-, End-to-End- und manuelle Tests.

Vollständiges Codebeispiel und Tests mit guter Codeabdeckung ansehen Sie können den Code und die Tests auch mit 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({});
  });
});