4 często spotykane typy pokrycia kodu

Dowiedz się, jaki jest zasięg kodu, i poznaj 4 typowe sposoby jego pomiaru.

Czy znasz wyrażenie „zasięg kodu”? W tym poście przypomnimy, jaki jest zasięg kodu w testach i poznamy 4 typowe sposoby jego pomiaru.

Czym jest pokrycie kodu?

Pokrycie kodu to wskaźnik określający, jaki odsetek kodu źródłowego jest wykonywany w testach. Pomaga on wykryć obszary, które mogą nie zostać przetestowane.

Rejestrowanie danych często wygląda tak:

Plik Procent wyrażenia Gałąź% % funkcji Wiersze (%) Nieodkryte 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 zwiększanie odsetka pokrycia kodu zwiększa pewność, że Twoja aplikacja została dokładnie przetestowana. Jest jednak jeszcze więcej do odkrycia.

4 typowe typy pokrycia kodu

Istnieją 4 typowe sposoby zbierania i obliczania zakresu kodu: funkcja, wiersz, gałąź i zasięg instrukcji.

Cztery typy pokrycia tekstu.

Aby zobaczyć, jak każdy typ pokrycia oblicza procent w zależności od rodzaju 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 weryfikujące 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({});
  });
});

Możesz uruchomić kod i testy w tej prezentacji na żywo lub sprawdzić w repozytorium.

Zasięg funkcji

Pokrycie kodu: 50%

/* coffee.js */

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

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

Zasięg funkcji to proste dane. Wykrywa odsetek funkcji w kodzie, które są wywoływane w ramach testu.

W przykładowym kodzie są 2 funkcje: calcCoffeeIngredient i isValidCoffee. Testy wywołują tylko funkcję calcCoffeeIngredient, więc zasięg funkcji 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);
}

Pokrycie linii mierzy odsetek wierszy kodu wykonywalnego wykonanych przez pakiet testowy. Jeśli wiersz kodu pozostaje niewykonany, oznacza to, że jego część nie została przetestowana.

Przykładowy kod zawiera 8 wierszy kodu wykonywalnego (zaznaczonych na czerwono i zielony), ale testy nie wykonują warunku americano (2 wiersze) ani funkcji isValidCoffee (1 wiersz). W efekcie pokrycie linii wynosi 62,5%.

Pamiętaj, że w pokryciu wiersza nie są uwzględniane instrukcje deklaracji, takie jak function isValidCoffee(name) i let espresso, water;, ponieważ nie są one wykonywalne.

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, na przykład instrukcji if lub pętli. Określa, czy testy sprawdzają zarówno prawdziwą, jak i fałszywą gałęzię stwierdzeń warunkowych.

Przykładowy kod ma 5 gałęzi:

  1. Dzwonię pod calcCoffeeIngredient, używając tylko coffeeName Iluzik.
  2. Dzwonię do: calcCoffeeIngredient, coffeeName i cup Iluzik.
  3. Kawa to espresso Iluzik.
  4. Kawa to americana Znak X.
  5. Inna kawa Iluzik.

Testy obejmują wszystkie gałęzie oprócz warunku Coffee is Americano. Pokrycie 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);
}

Pokrycie oświadczenia pokazuje odsetek instrukcji w kodzie, które są wykonywane w ramach testów. Na pierwszy rzut oka można pomyśleć: „Czy to nie jest to samo z zasięgiem wiersza?”. Zasięg wyciągu jest w rzeczywistości podobny do pokrycia wiersza, ale uwzględnia pojedyncze wiersze kodu, które zawierają wiele instrukcji.

Przykładowy kod zawiera 8 wierszy kodu wykonywalnego i 9 instrukcji. Czy rozpoznasz wiersz zawierający dwa wyrażenia?

Sprawdź swoją odpowiedź

To ten wiersz: espresso = 30 * cup; water = 70 * cup;

Testy obejmują tylko pięć z dziewięciu stwierdzeń, więc pokrycie danych wynosi 55,55%.

Jeśli zawsze piszesz po jednej instrukcji w każdym wierszu, zasięg wiersza będzie podobny do pokrycia wyciągu.

Jaki typ pokrycia kodu wybierzesz?

Większość narzędzi do wykrywania kodu obejmuje 4 typy typowych zastosowań kodu. Wybór wskaźnika pokrycia kodu, który należy traktować priorytetowo, zależy od konkretnych wymagań projektu, metod programowania i celów testowania.

Ogólnie rzecz biorąc, warto zacząć od pokrycia danych, ponieważ jest to proste i zrozumiałe dane. W odróżnieniu od pokrycia instrukcji zasięg gałęzi i zasięg funkcji określają, czy testy wywołują warunek (gałąź) czy funkcję. Jest to więc naturalny postęp, który następuje po pokryciach instrukcji.

Gdy uzyskasz wysoki zasięg instrukcji, możesz przejść do pokrycia gałęzi i funkcji.

Czy zakres testu jest taki sam jak zakres kodu?

Nie. Zakres testu i zasięg kodu często się mylą, ale różnią się od siebie:

  • Zasięg testu: dane wodne, 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, jaki fragment kodu obejmują testy.

Oto uproszczona analogia: wyobraź sobie aplikację internetową jako dom.

  • Zakres testu pokazuje, jak dobrze testy obejmują pomieszczenia w domu.
  • Zasięg kodu pokazuje, jaka część domu została sprawdzona podczas testów.

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

Chociaż uzyskanie dużego pokrycia kodu podczas testowania jest z pewnością pożądane, 100% pokrycia kodu nie gwarantuje braku błędów w kodzie.

Bezsensowny sposób na uzyskanie 100% pokrycia kodu

Przeanalizuj 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, linii, gałęzi i instrukcji, ale nie ma sensu, ponieważ w rzeczywistości kod nie jest testowany. Asercja expect(true).toBe(true) zawsze przejdzie niezależnie od tego, czy kod działa prawidłowo.

Zły wskaźnik jest gorszy niż brak wartości.

Błędne dane mogą dawać błędne poczucie bezpieczeństwa, co jest gorsze niż brak wartości w ogóle. Jeśli na przykład masz pakiet testowy, który zapewnia 100% pokrycia kodu, ale testy nie mają sensu, możesz otrzymać fałszywe poczucie bezpieczeństwa, że kod jest dobrze przetestowany. Jeśli przypadkowo usuniesz lub uszkodzisz fragment kodu aplikacji, testy nie zostaną przerwane, mimo że aplikacja nie będzie już działać prawidłowo.

Aby tego uniknąć:

  • Testy. Pisz i przeglądaj testy, aby upewnić się, że są istotne, i przetestuj kod w różnych sytuacjach.
  • Używaj pokrycia kodu jako wskazówki, a nie jedynej miary skuteczności testu lub jakości kodu.

Wykorzystanie pokrycia kodu w różnych typach testów

Przyjrzyjmy się bliżej temu, jak można wykorzystać pokrycie kodu w ramach 3 najczęściej używanych typów testów:

  • Testy jednostkowe. Stanowią najlepszy typ testu do zbierania pokrycia kodu, ponieważ zostały zaprojektowane z myślą o uwzględnieniu wielu małych scenariuszy i ścieżek testów.
  • Testy integracji. Mogą pomóc zbierać informacje o kodzie na potrzeby testów integracji, ale należy używać ich z rozwagą. W takim przypadku obliczasz pokrycie większej części kodu źródłowego i trudno określić, które testy faktycznie obejmują które części kodu. Obliczenie zakresu kodu testów integracji może jednak być przydatne w przypadku starszych systemów, które nie mają dobrze izolowanych jednostek.
  • Testy kompleksowe. Pomiar pokrycia kodu w ramach testów E2E jest trudny i trudny ze względu na złożony charakter testów. Zamiast obejmować zakres kodu, lepszym rozwiązaniem może być spełnienie wymagań, ponieważ testy E2E mają na celu spełnienie wymagań testu, a nie skupienie się na kodzie źródłowym.

Podsumowanie

Pokrycie kodu może być przydatnym wskaźnikiem skuteczności testów. Może pomóc w poprawie jakości aplikacji, upewniając się, że najważniejsze zasady w kodzie zostały dobrze przetestowane.

Pamiętaj jednak, że zasięg kodu to tylko 1 wskaźnik. Weź też pod uwagę inne czynniki, takie jak jakość testów i wymagania dotyczące aplikacji.

Dążenie do pełnego pokrycia kodu nie jest celem. Zamiast tego warto korzystać z pełnego kodu wraz z dobrze skonstruowanym planem testowania obejmującym różne metody testowania, w tym testy jednostkowe, integracyjne, kompleksowe i ręczne.

Zapoznaj się z pełnym przykładem kodu i testami, które zapewnią dobry zasięg kodu. Kod i testy możesz też uruchomić w 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({});
  });
});