Dowiedz się, czym jest zasięg kodu, i poznaj 4 popularne sposoby jego pomiaru.
Czy znasz określenie „zasięg kodu”? W tym poście omówimy zasięg kodu w testach i 4 popularne sposoby jego pomiaru.
Co to jest zasięg kodu?
Pokrycie kodu to wskaźnik określający odsetek kodu źródłowego wykonywanego przez testy. Dzięki temu łatwiej zidentyfikujesz obszary, w których brakuje odpowiednich testów.
Rejestrowanie tych wskaźników często wygląda tak:
Plik | % wyciągów | % Gałąź | % funkcji | % Linie | 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. To jednak nie wszystko.
4 najczęstsze typy pokrycia kodu
Istnieją 4 popularne sposoby zbierania i obliczania pokrycia kodu: wykorzystanie funkcji, wiersza, gałęzi i pokrycia instrukcji.
Aby dowiedzieć się, jak oblicza się wartość procentową w przypadku poszczególnych typów pokrycia kodu, zapoznaj się z tym przykładowym kodem służącym 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 ramach tej prezentacji na żywo lub sprawdzić repozytorium.
Zakres funkcji
Pokrycie kodu: 50%
/* coffee.js */
export function calcCoffeeIngredient(coffeeName, cup = 1) {
// ...
}
function isValidCoffee(name) {
// ...
}
Zasięg funkcji to proste dane. Przechwytuje odsetek funkcji w kodzie wywoływanych przez testy.
W przykładowym kodzie są 2 funkcje: calcCoffeeIngredient
i isValidCoffee
. Testy wywołują tylko funkcję calcCoffeeIngredient
, więc zasięg 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);
}
Pokrycie wierszy mierzy odsetek wierszy kodu wykonywalnego, które zostały wykonane przez Twój pakiet testowy. Jeśli wiersz kodu pozostaje niewykonany, oznacza to, że jakaś część kodu nie została przetestowana.
Przykładowy kod zawiera 8 wierszy kodu wykonywalnego (zaznaczone na czerwono i zielony), ale testy nie wykonują warunku americano
(2 wiersze) ani funkcji isValidCoffee
(1 wiersz). Daje to pokrycie linii na poziomie 62,5%.
Pamiętaj, że zasięg wiersza nie uwzględnia instrukcji deklaracji, takich jak function isValidCoffee(name)
i let espresso, water;
, ponieważ nie są one wykonywalne.
Zasięg oddziałów
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 badają zarówno prawdziwe, jak i fałszywe gałęzie twierdzeń warunkowych.
W przykładowym kodzie jest pięć gałęzi:
- Dzwonię do:
calcCoffeeIngredient
za jedynecoffeeName
- Dzwonię do:
calcCoffeeIngredient
z użytkownikamicoffeeName
icup
- Kawa to espresso
- Kawa jest Americano
- Inna kawa
Testy obejmują wszystkie gałęzie oprócz warunku Coffee is Americano
. Zasięg oddziałów wynosi więc 80%.
Zakres na 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);
}
Pokrycie deklaracji mierzy odsetek instrukcji w kodzie, które są wykonywane przez testy. Na pierwszy rzut oka możesz się zastanawiać, czy to nie to samo co zasięg linii. Zasięg na wyciągu jest podobny do pokrycia wierszy, ale uwzględnia pojedyncze wiersze kodu, które zawierają wiele instrukcji.
Przykładowy kod zawiera 8 wierszy kodu wykonywalnego oraz dziewięć instrukcji. Czy widzisz wiersz z 2 instrukcjami?
espresso = 30 * cup; water = 70 * cup;
Testy obejmują tylko 5 z dziewięciu stwierdzeń, więc zasięg stwierdzenia wynosi 55,55%.
Jeśli zawsze podasz po jednej instrukcji w wierszu, pokrycie wiersza będzie podobne do pokrycia na wyciągu.
Jaki typ pokrycia kodu wybierzesz?
Większość narzędzi pokrycia kodu obejmuje te 4 typy wspólnego pokrycia kodu. Wybór wskaźnika pokrycia kodu zależy od konkretnych wymagań projektu, metod programistycznych i celów testowania.
Ogólnie dobrym punktem wyjścia jest pokrycie kosztów na podstawie wyrażeń, ponieważ jest to prosty i przystępny wskaźnik. W przeciwieństwie do pokrycia instrukcji zasięg gałęzi i zasięg funkcji mierzą, czy testy wywołują warunek (gałąź) czy funkcję. Są więc naturalnym postępem po uwzględnieniu wyrażeń.
Gdy uzyskasz duży zasięg instrukcji, możesz przejść do zasięgu oddziałów i zasięgu funkcji.
Czy zasięg testu jest taki sam jak zasięg kodu?
Nie. Zasięg testu i zasięg kodu są często mylone, ale różnią się od siebie:
- Zakres testów: dane akwarytacyjne, które określają, w jakim stopniu pakiet testowy obejmuje funkcje oprogramowania. Pomaga 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 aplikację internetową jako dom.
- Zasięg testów pokazuje, w jakim stopniu testy obejmują pomieszczenia w domu.
- Pokrycie kodu pozwala określić, jaka część domu została przejęta przez testy.
100% pokrycia kodu nie oznacza braku błędów
Chociaż pokrycie kodu jest z pewnością pożądane, 100% pokrycia kodu nie gwarantuje braku błędów ani wad.
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-procentowe pokrycie funkcji, linii, 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
Nieprawidłowe dane mogą sugerować błędne poczucie bezpieczeństwa, co jest gorsze od braku danych. Jeśli na przykład masz pakiet testowy, który osiąga 100% pokrycia kodu, ale testy nie mają sensu, możesz mieć błędne poczucie bezpieczeństwa, że 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 tego uniknąć:
- Weryfikacja testowa. Pisz i przeglądaj testy, aby upewnić się, że są przydatne, i testuj kod w różnych sytuacjach.
- 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 sposobom wykorzystania pokrycia kodu w przypadku 3 najpopularniejszych rodzajów testów:
- Testy jednostkowe. Są one najlepszym typem testowania do gromadzenia danych o pokryciu kodu, ponieważ są przeznaczone do obsługi wielu małych scenariuszy i ścieżek testowania.
- Testy integracji. Mogą one pomóc w zbieraniu danych o pokryciu kodu na potrzeby testów integracji, ale należy ich używać z rozwagą. 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. Obliczanie zasięgu kodu w testach integracji może jednak być przydatne w przypadku starszych systemów, które nie mają dobrze odizolowanych jednostek.
- Kompleksowe testy (E2E). Ze względu na skomplikowany charakter testów pomiar pokrycia kodu w ramach testów E2E jest trudny i trudny. Zamiast użycia pokrycia kodu lepszą opcją może być pokrycie wymagań. Wynika to z faktu, że testy E2E skupiają się na spełnieniu wymagań testu, a nie na kodzie źródłowym.
Podsumowanie
Pokrycie kodu może być przydatnym wskaźnikiem do pomiaru 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 zasięg kodu to tylko jeden rodzaj danych. Musisz też wziąć pod uwagę inne czynniki, takie jak jakość testów i wymagania aplikacji.
Osiągnięcie 100% pokrycia kodu nie jest celem. Zamiast tego zalecamy stosowanie zakresu kodu w połączeniu z wszechstronnym planem testowania obejmującym różne metody testowania, w tym testy jednostkowe, integracyjne, testy kompleksowe i testy ręczne.
Zobacz przykład całego kodu i testy z dobrym pokryciem 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({});
});
});