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 artykule omówimy, czym jest pokrycie kodu w testach, i 4 najczęstsze sposoby jego pomiaru.
Co to jest pokrycie kodu?
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 | % instrukcji | % Branch | Funkcje (%) | % linii | Nieobjęte linie |
---|---|---|---|---|---|
file.js | 90% | 100% | 90% | 80% | 89 256 |
coffee.js | 55,55% | 80% | 50% | 62,5% | 10–11, 18 |
Dodając nowe funkcje i testy, możesz zwiększyć procentowy zasięg kodu, co pozwoli Ci zyskać 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: pokrycie funkcji, linii, gałęzi i instrukcji.
Aby zobaczyć, jak każdy typ pokrycia kodu oblicza swój odsetek, rozważ ten przykład kodu służący 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) {
// ...
}
Pokrycie funkcji to prosta miara. Określa on odsetek funkcji w Twoim kodzie, które są wywoływane przez testy.
W przykładowym kodzie występują 2 funkcje: calcCoffeeIngredient
i isValidCoffee
. Testy wywołują tylko funkcję calcCoffeeIngredient
, więc jej pokrycie wynosi 50%.
Zasięg 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 kodu mierzy odsetek linii kodu wykonywalnego, który został wykonany 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). W rezultacie pokrycie linii wynosi 62,5%.
Zasięg linii nie uwzględnia instrukcji deklaracji, takich jak function isValidCoffee(name)
i let espresso, water;
, ponieważ nie są one uruchamiane.
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, np. instrukcji warunkowych if lub pętli. Określa, czy testy sprawdzają zarówno gałęzie prawdziwe, jak i fałszywe instrukcji warunkowych.
W przykładowym kodzie jest 5 gałęzi:
- Dzwonię pod numer
calcCoffeeIngredient
, wpisując tylkocoffeeName
- Dzwonię z
calcCoffeeIngredient
docoffeeName
icup
- Kawa to espresso
- Kawa to Americano
- Inna kawa:
Testy obejmują wszystkie gałęzie z wyjątkiem warunku Coffee is Americano
. Oznacza to, że pokrycie gałęzi wynosi 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?
espresso = 30 * cup; water = 70 * cup;
Testy obejmują tylko 5 z 9 oświadczeń, więc pokrycie testów wynosi 55,55%.
Jeśli zawsze wpisujesz 1 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 rodzaju pokrycia kodu, któremu ma być przypisana najwyższa priorytet, zależy od konkretnych wymagań projektu, metod tworzenia oprogramowania 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 pokrycie testów jest takie samo jak pokrycie kodu?
Nie. Pojęcia „zakres testowania” i „zakres testowania kodu” są często mylone, ale są to różne pojęcia:
- Zakres testów: mianowa wielkość określająca, jak dobrze pakiet testów obejmuje funkcje oprogramowania. Pomaga to określić poziom ryzyka.
- Pokrycie kodu: dane ilościowe, które określają proporcję kodu wykonanego 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% pokrycia kodu nie gwarantuje braku błędów ani wad w kodzie.
Bezsensowny sposób na osiągnięcie 100% zasięgu 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. Założenie expect(true).toBe(true)
zawsze się powiedzie, niezależnie od tego, czy kod działa prawidłowo.
Złe dane są gorsze niż brak danych
Zła miara może dać Ci fałszywe poczucie bezpieczeństwa, co jest gorsze niż brak jakichkolwiek 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 część kodu aplikacji, testy będą nadal przechodzić, mimo że aplikacja nie będzie już działać prawidłowo.
Aby uniknąć tego scenariusza:
- Testowanie opinii Napisz i sprawdź testy, aby mieć pewność, że są one sensowne, i przetestuj kod w różnych scenariuszach.
- Używaj pokrycia kodu jako wskazówki, a nie jako jedynego wskaźnika skuteczności testów lub jakości kodu.
Używanie pokrycia kodu w różnych rodzajach testów
Przyjrzyjmy się bliżej temu, jak możesz wykorzystywać pokrycie kodu w 3 najczęstszych typach testów:
- Testy jednostkowe. To najlepszy typ testu do zbierania danych o zasięgu kodu, ponieważ obejmuje on wiele małych scenariuszy i ścieżek testowych.
- testy integracyjne. Mogą one pomóc w uzyskaniu pokrycia kodu w przypadku testów integracji, ale należy ich używać z ostrożnością. W tym przypadku obliczasz pokrycie większej części kodu źródłowego, a określenie, które testy obejmują poszczególne 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.
- Testy kompleksowe (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. Pomoże Ci to poprawić jakość aplikacji, ponieważ zapewni Ci pewność, że kluczowa logika w kodzie 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.
Nie jest to jednak 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 demo 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({});
});
});