4 często spotykane typy pokrycia kodu

Dowiedz się, czym jest pokrycie kodu, i poznaj 4 najczęstsze sposoby jego pomiaru.

Czy słyszałeś/słyszałaś termin „zasięg kodu”? W tym poście omówimy, czym jest pokrycie kodu w testach i cztery najpopularniejsze sposoby jego pomiaru.

Zakres testowania kodu to dane określające odsetek kodu źródłowego, który jest wykonywany przez testy. Pomaga to zidentyfikować obszary, które mogą nie być odpowiednio przetestowane.

Często rejestrowanie tych danych wygląda tak:

Plik % wyciągów % Branch % funkcji % linii Niepokryte linie
file.js 90%, 100% 90%, 80% 89 256
coffee.js 55,55% 80% 50% 62,5% 10–11, 18

W miarę dodawania nowych funkcji i testów większy odsetek pokrycia kodu może dać większą pewność, że aplikacja została dokładnie przetestowana. Możesz jednak odkryć jeszcze więcej.

Cztery najpopularniejsze typy pokrycia kodu

Istnieją 4 popularne sposoby zbierania i obliczania pokrycia kodu: wykorzystanie funkcji, wiersza, gałęzi i pokrycia instrukcji.

4 typy zasięgu tekstu.

Aby zobaczyć, jak każdy typ pokrycia kodu oblicza swój odsetek, rozważ ten przykład kodu służącego do obliczania składników kawy:

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

Testy, które weryfikują funkcję calcCoffeeIngredient:

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

Kod i testy możesz uruchomić w demo na żywo lub pobrać z repozytorium.

Zasięg funkcji

Pokrycie kodu: 50%

/* coffee.js */

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

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

Zasięg funkcji to proste dane. Określa on odsetek funkcji w Twoim kodzie, które są wywoływane przez testy.

W przykładowym kodzie są 2 funkcje: calcCoffeeIngredient i isValidCoffee. Testy wywołują tylko funkcję calcCoffeeIngredient, więc pokrycie funkcji wynosi 50%.

Pokrycie linii

Pokrycie kodu: 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);
}

Zakres linii mierzy odsetek linii kodu wykonywalnego, które zostały wykonane przez zestaw testów. Jeśli jakiś wiersz kodu nie został wykonany, oznacza to, że część kodu nie została przetestowana.

Przykładowy kod zawiera 8 wierszy kodu wykonywalnego (zaznaczone na czerwono i zielono), ale testy nie wykonują warunku americano (2 wiersze) ani funkcji isValidCoffee (1 wiersz). Daje to pokrycie linii na poziomie 62,5%.

Zasięg linii nie uwzględnia instrukcji deklaracji, takich jak function isValidCoffee(name)let espresso, water;, ponieważ nie można ich wykonać.

Zasięg gałęzi

Pokrycie kodu: 80%

/* coffee.js */

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

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

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

  return {};
}

Pokrycie gałęzi mierzy odsetek wykonanych gałęzi lub punktów decyzji w kodzie, takich jak instrukcje lub pętle. Określa, czy testy sprawdzają zarówno gałęzie prawdziwe, jak i fałszywe instrukcji warunkowych.

W przykładowym kodzie jest 5 gałęzi:

  1. Dzwonię pod numer calcCoffeeIngredient, wpisując tylko coffeeName Znacznik wyboru.
  2. Łączę z: calcCoffeeIngredient, coffeeName i cup Znacznik wyboru.
  3. Kawa to espresso Znak oczekiwania.
  4. Kawa jest Americano Znak X.
  5. Inna kawa: Znak oczekiwania.

Testy obejmują wszystkie gałęzie z wyjątkiem gałęzi z warunkiem Coffee is Americano. Zasięg oddziałów wynosi więc 80%.

Zakres wyciągu

Pokrycie kodu: 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);
}

Zakres instrukcji mierzy odsetek instrukcji w kodzie, które są wykonywane przez testy. Na pierwszy rzut oka możesz pomyśleć, że jest to to samo co pokrycie instrukcji. W rzeczy samej pokrycie instrukcji jest podobne do pokrycia wiersza, ale uwzględnia pojedyncze wiersze kodu, które zawierają wiele instrukcji.

W przykładowym kodzie jest 8 wierszy kodu wykonywalnego, ale 9 instrukcji. Czy widzisz wiersz zawierający 2 oświadczenia?

Jest to ten wiersz: espresso = 30 * cup; water = 70 * cup;

Testy obejmują tylko 5 z 9 oświadczeń, więc ich pokrycie wynosi 55,55%.

Jeśli zawsze wpisujesz jedno oświadczenie na wiersz, pokrycie wiersza będzie podobne do pokrycia oświadczenia.

Jaki typ pokrycia kodu wybrać?

Większość narzędzi do testowania pokrycia kodu obejmuje te 4 typy testowania pokrycia kodu. Wybór wskaźnika pokrycia kodu zależy od konkretnych wymagań projektu, metod programistycznych i celów testowania.

Zasadniczo warto zacząć od pokrycia stwierdzenia, ponieważ jest to prosta i łatwa do zrozumienia miara. W przeciwieństwie do pokrycia instrukcji pokrycie gałęzi i funkcja wskazują, czy testy wywołują warunek (gałąź) czy funkcję. Dlatego są one naturalnym rozwinięciem po oświadczeniu.

Gdy osiągniesz wysoki poziom pokrycia instrukcji, możesz przejść do pokrycia gałęzi i funkcji.

Czy zasięg testu jest taki sam jak zasięg kodu?

Nie. Pojęcia „zakres testowania” i „zakres testowania kodu” są często mylone, ale są to różne pojęcia:

  • Zakres testów: dane akwarytalne, które określają, w jakim stopniu pakiet testowy obejmuje funkcje oprogramowania. Pomaga to określić poziom ryzyka.
  • Pokrycie kodu: dane ilościowe służące do pomiaru odsetka kodu wykonywanego podczas testowania. Chodzi o to, ile kodu obejmują testy.

Oto uproszczona analogia: wyobraź sobie, że aplikacja internetowa to dom.

  • Zakres testów mierzy, jak dobrze testy obejmują pomieszczenia w domu.
  • Zakres testowania kodu mierzy, jak dużą część kodu obejmują testy.

100% pokrycia kodu nie oznacza braku błędów

Chociaż osiągnięcie wysokiego pokrycia kodu podczas testów jest bardzo pożądane, 100% pokryta kodu nie gwarantuje braku błędów ani wad w kodzie.

bezsensowny sposób na 100% pokrycia kodu;

Rozważ ten 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
  });
});

Ten test osiąga 100% pokrycia funkcji, wierszy, gałęzi i instrukcji, ale nie ma sensu, ponieważ nie testuje kodu. Potwierdzenie expect(true).toBe(true) zawsze zakończy się bez względu na to, czy kod działa prawidłowo.

Zły wskaźnik jest gorszy niż żaden

Złe dane mogą dawać fałszywe poczucie bezpieczeństwa, co jest gorsze od braku danych. Jeśli na przykład masz zestaw testów, który osiąga 100% zasięgu kodu, ale wszystkie testy są bez znaczenia, możesz mieć fałszywe poczucie bezpieczeństwa, że Twój kod jest dobrze przetestowany. Jeśli przypadkowo usuniesz lub uszkodzisz fragment kodu aplikacji, testy również nie zakończą się powodzeniem, mimo że aplikacja nie będzie już działać prawidłowo.

Aby uniknąć tego scenariusza:

  • Weryfikacja testowa. Napisz i sprawdź testy, aby mieć pewność, że są one sensowne, i przetestuj kod w różnych scenariuszach.
  • Wykorzystaj zasięg kodu jako wytyczne, a nie jako jedyną miarę skuteczności testu lub jakości kodu.

Wykorzystanie zasięgu kodu w różnych rodzajach testowania

Przyjrzyjmy się bliżej temu, jak możesz wykorzystywać pokrycie kodu w 3 najczęstszych typach testów:

  • Testy jednostkowe. Są one najlepszym typem testowania do gromadzenia danych o pokryciu kodu, ponieważ są zaprojektowane z myślą o wielu drobnych scenariuszach i ścieżkach testowania.
  • testy integracji, Mogą one pomóc w zbieraniu danych o zasięgu kodu na potrzeby testów integracyjnych, ale należy ich używać z ostrożnością. W takim przypadku obliczysz pokrycie większej części kodu źródłowego, a ustalenie, które testy faktycznie obejmują te części kodu, może być trudne. Mimo to obliczanie pokrycia kodu w przypadku testów integracyjnych może być przydatne w przypadku starszych systemów, które nie mają dobrze odizolowanych jednostek.
  • Kompleksowe testy (E2E). Pomiar pokrycia kodu w przypadku testów E2E jest trudny i wymagający ze względu na złożoną naturę tych testów. Zamiast testowania pokrycia kodu lepiej jest stosować testowanie pokrycia wymagań. Dzieje się tak, ponieważ testy E2E mają na celu spełnienie wymagań testu, a nie skupianie się na kodzie źródłowym.

Podsumowanie

Zasięg kodu może być przydatnym wskaźnikiem skuteczności testów. Może pomóc w poprawie jakości aplikacji, upewniając się, że kluczowa logika kodu jest dobrze przetestowana.

Pamiętaj jednak, że pokrycie kodu to tylko jeden wskaźnik. Weź pod uwagę też inne czynniki, takie jak jakość testów i wymagania aplikacji.

Osiągnięcie 100% pokrycia kodu nie jest celem. Zamiast tego należy stosować pokrycie kodu oraz wszechstronny plan testów, który obejmuje różne metody testowania, w tym testy jednostkowe, testy integracji, testy kompleksowe i testy ręczne.

Zapoznaj się z pełnym przykładowym kodem i testami o dobrym pokryciu kodu. Kod i testy możesz też uruchomić w ramach tej prezentacji na żywo.

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