Vier gängige Arten der Codeabdeckung

Hier erfahren Sie, was Codeabdeckung ist, und lernen vier gängige Methoden zur Messung der Abdeckung kennen.

Kennen Sie den Begriff „Codeabdeckung“? In diesem Beitrag erfahren Sie, was Codeabdeckung in Tests ist und welche vier gängigen Methoden zu ihrer Messung verwendet werden.

Was ist Codeabdeckung?

Die Codeabdeckung ist ein Messwert, der den Prozentsatz des Quellcodes misst, der von Ihren Tests ausgeführt wird. Es hilft Ihnen, Bereiche zu identifizieren, in denen möglicherweise keine ordnungsgemäßen Tests vorhanden sind.

Das Aufzeichnen dieser Messwerte sieht häufig so aus:

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, kann eine Erhöhung des Prozentsatzes für die Codeabdeckung Ihnen mehr Gewissheit geben, dass Ihre Anwendung gründlich getestet wurde. Aber es gibt noch mehr zu entdecken.

Vier gängige Arten von Codeabdeckung

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

Vier Arten von Textabdeckung.

Sehen Sie sich das folgende Codebeispiel zur Berechnung von Kaffeezutaten an, um zu sehen, wie jede Art von Codeabdeckung ihren Prozentsatz berechnet:

/* 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, mit denen die calcCoffeeIngredient-Funktion verifiziert wird, 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 sich das Repository ansehen.

Funktionsabdeckung

Codeabdeckung: 50%

/* coffee.js */

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

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

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

Im Codebeispiel gibt es zwei Funktionen: calcCoffeeIngredient und isValidCoffee. Die Tests rufen nur die calcCoffeeIngredient-Funktion 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);
}

Die Zeilenabdeckung misst den Prozentsatz der ausführbaren Codezeilen, die Ihre Testsuite ausgeführt hat. 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 Code (rot und grün hervorgehoben), aber die Tests führen die Bedingung americano (zwei Zeilen) und die Funktion isValidCoffee (eine Zeile) nicht aus. Dies führt zu einer Linienabdeckung von 62,5%.

Bei der Zeilenabdeckung werden keine Erklärungen wie function isValidCoffee(name) und let espresso, water; 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 Zweigabdeckung misst den Prozentsatz der ausgeführten Zweige oder Entscheidungspunkte im Code, z. B. wenn-Anweisungen oder Schleifen. Damit wird bestimmt, ob bei Tests sowohl der True- als auch der False-Zweig von bedingten Anweisungen untersucht werden.

Das Codebeispiel enthält fünf Zweige:

  1. calcCoffeeIngredient wird mit nur coffeeName Häkchen. angerufen
  2. calcCoffeeIngredient wird mit coffeeName und cup Häkchen. angerufen
  3. Kaffee ist Espresso Häkchen.
  4. Kaffee ist Americano X.
  5. Anderer Kaffee Häkchen.

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

Abdeckung von Kontoauszügen

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);
}

Abdeckung von Anweisungen misst den Prozentsatz der Anweisungen in Ihrem Code, die Ihre Tests ausführen. Auf den ersten Blick fragen Sie sich vielleicht: „Ist das nicht dasselbe wie die Zeilenabdeckung?“ Tatsächlich ist die Abdeckung ähnlich wie die Zeilenabdeckung, es werden aber einzelne Codezeilen berücksichtigt, die mehrere Anweisungen enthalten.

Das Codebeispiel enthält acht Zeilen ausführbaren Code, aber es gibt neun Anweisungen. Erkennst du die Zeile, die zwei Aussagen enthält?

Antwort prüfen

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

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

Wenn Sie immer eine Anweisung pro Zeile schreiben, ist die Linienabdeckung der Abdeckung ähnlich.

Welche Art von Codeabdeckung sollten Sie wählen?

Die meisten Tools zur Codeabdeckung umfassen diese vier Arten der allgemeinen Codeabdeckung. Die Entscheidung, welcher Messwert für die Codeabdeckung priorisiert werden soll, hängt von den spezifischen Projektanforderungen, Entwicklungspraktiken und Testzielen ab.

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

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

Ist die Testabdeckung dasselbe wie die Codeabdeckung?

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

  • Testabdeckung: Messwert, der angibt, wie gut die Testsuite die Funktionen der Software abdeckt. Es hilft dabei, 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 ein Haus vor.

  • Mit der Testabdeckung wird gemessen, wie gut die Tests die Räume im Haus abdecken.
  • Die Codeabdeckung gibt an, wie viel vom Haus die Tests durchlaufen haben.

100% Codeabdeckung bedeutet nicht, dass es Programmfehler gibt

Zwar ist es sicherlich wünschenswert, eine hohe Codeabdeckung in Tests zu erzielen, eine 100-%-Codeabdeckung garantiert jedoch nicht das Fehlen von Fehlern oder Fehlern in Ihrem Code.

Eine bedeutungslose Möglichkeit, 100% Codeabdeckung zu erzielen

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 100% ige Abdeckung von Funktionen, Linien, Zweigen und Anweisungen. Er 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 richtig funktioniert.

Ein schlechter Messwert ist schlechter als kein Messwert.

Eine schlechte Metrik kann ein falsches Sicherheitsgefühl vermitteln, was noch schlimmer ist, als keine Metrik zu haben. Wenn Sie beispielsweise eine Testsuite haben, die eine Codeabdeckung von 100% erreicht, aber alle Tests bedeutungslos sind, haben Sie möglicherweise das falsche Sicherheitsgefühl, dass Ihr Code gut getestet wurde. Wenn Sie versehentlich einen Teil des Anwendungscodes löschen oder beschädigen, werden die Tests trotzdem erfolgreich durchgeführt, auch wenn die Anwendung nicht mehr ordnungsgemäß funktioniert.

So vermeiden Sie dieses Szenario:

  • Testrezension Schreiben und prüfen Sie Tests, um sicherzustellen, dass sie aussagekräftig sind, und testen Sie den Code in verschiedenen Szenarien.
  • Die Codeabdeckung dient als Richtlinie und nicht als einziger Messwert für die Testeffektivität oder die Codequalität.

Codeabdeckung in 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 eignen sich am besten zum Erfassen der Codeabdeckung, da sie mehrere kleine Szenarien und Testpfade abdecken sollen.
  • Integrationstests. Sie können beim Erfassen der Codeabdeckung für Integrationstests helfen, aber sie sollten mit Vorsicht verwendet werden. In diesem Fall berechnen Sie die Abdeckung eines größeren Teils des Quellcodes und es kann schwierig sein zu bestimmen, welche Tests welche Teile des Codes tatsächlich abdecken. Dennoch 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): Aufgrund der Komplexität dieser Tests ist es schwierig und schwierig, die Codeabdeckung für E2E-Tests zu messen. Anstatt die Codeabdeckung zu verwenden, ist die Anforderungsabdeckung möglicherweise die bessere Wahl, da der Schwerpunkt bei E2E-Tests darin besteht, die Anforderungen Ihres Tests abzudecken, und nicht auf den Quellcode.

Fazit

Die Codeabdeckung kann ein nützlicher Messwert sein, um die Effektivität Ihrer Tests zu messen. Es kann Ihnen helfen, die Qualität Ihrer Anwendung zu verbessern, indem sichergestellt wird, dass die entscheidende 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 Ihre Anwendungsanforderungen.

Das Ziel einer Codeabdeckung von 100% ist nicht das Ziel. Stattdessen sollten Sie die Codeabdeckung zusammen mit einem umfassenden Testplan verwenden, der eine Vielzahl von Testmethoden einschließt, darunter Unittests, Integrationstests, End-to-End-Tests 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({});
  });
});