Vier gängige Arten der Codeabdeckung

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

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, mit dem der Prozentsatz des Quellcodes gemessen wird, der in Ihren Tests ausgeführt wird. Es hilft Ihnen, Bereiche zu identifizieren, in denen möglicherweise keine ordnungsgemäßen Tests durchgeführt wurden.

Häufig sieht die Erfassung dieser Messwerte so aus:

Datei %-Angaben % Branch %-Funktionen % 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. 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 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. Daraus ergibt sich 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.

Im Codebeispiel gibt es fünf Zweige:

  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. Sonstiger Kaffee Häkchen

Die Tests decken alle Branches 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 fragen Sie sich vielleicht: „Ist das nicht dasselbe wie 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. Sehen Sie die Zeile mit zwei Anweisungen?

Das ist die folgende Zeile: 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 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 misst die Zweigabdeckung und die Funktionsabdeckung, ob Tests eine Bedingung (Zweig) 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: Aqualitativer 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, wie gut 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 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 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. Der Schwerpunkt von End-to-End-Tests liegt nämlich darauf, die Anforderungen des Tests abzudecken, 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.

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({});
  });
});
{/20