Четыре распространенных типа покрытия кода

Узнайте, что такое покрытие кода, и найдите четыре распространенных способа его измерения.

Слышали ли вы фразу «покрытие кода»? В этом посте мы рассмотрим, что такое покрытие кода в тестах, и четыре распространенных способа его измерения.

Что такое покрытие кода?

Покрытие кода — это показатель, который измеряет процент исходного кода, выполняемого вашими тестами. Это поможет вам определить области, в которых может не хватать надлежащего тестирования.

Часто запись этих метрик выглядит так:

Файл % заявлений % Ветвь % функций % строк Непокрытые линии
файл.js 90% 100% 90% 80% 89 256
кофе.js 55,55% 80% 50% 62,5% 10-11, 18

По мере добавления новых функций и тестов увеличение процента покрытия кода может дать вам больше уверенности в том, что ваше приложение было тщательно протестировано. Однако есть еще кое-что, что можно открыть.

Четыре распространенных типа покрытия кода

Существует четыре распространенных способа сбора и расчета покрытия кода: покрытие функций, строк, ветвей и операторов.

Четыре типа текстового покрытия.

Чтобы увидеть, как каждый тип покрытия кода рассчитывает процентное соотношение, рассмотрим следующий пример кода для расчета ингредиентов кофе:

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

Тесты, проверяющие функцию 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({});
  });
});

Вы можете запустить код и тесты в этой живой демонстрации или просмотреть репозиторий .

Функциональный охват

Покрытие кода: 50%

/* coffee.js */

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

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

Покрытие функции — это простой показатель. Он фиксирует процент функций в вашем коде, которые вызывают ваши тесты.

В примере кода есть две функции: calcCoffeeIngredient и isValidCoffee . Тесты вызывают только функцию calcCoffeeIngredient , поэтому покрытие функции составляет 50%.

Покрытие линии

Покрытие кода: 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);
}

Покрытие строк измеряет процент строк исполняемого кода, выполненных вашим набором тестов. Если строка кода остается невыполненной, это означает, что какая-то часть кода не была протестирована.

Пример кода содержит восемь строк исполняемого кода (выделены красным и зеленым), но тесты не выполняют условие americano (две строки) и функцию isValidCoffee (одна строка). В результате покрытие линии составляет 62,5%.

Обратите внимание, что покрытие строк не учитывает операторы объявления, такие как function isValidCoffee(name) и let espresso, water; , потому что они не являются исполняемыми.

Покрытие филиалов

Покрытие кода: 80%

/* coffee.js */

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

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

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

  return {};
}

Покрытие ветвей измеряет процент выполненных ветвей или точек принятия решений в коде, например операторов if или циклов. Он определяет, проверяют ли тесты как истинные, так и ложные ветви условных операторов.

В примере кода есть пять ветвей:

  1. Вызов calcCoffeeIngredient с использованием только coffeeName Чек отметка.
  2. Вызов calcCoffeeIngredient с coffeeName и cup Чек отметка.
  3. Кофе – это эспрессо Чек отметка.
  4. Кофе американо Знак Х.
  5. Другой кофе Чек отметка.

Тесты охватывают все отрасли, кроме сорта Coffee is Americano . Таким образом, охват филиалов составляет 80%.

Покрытие заявлений

Покрытие кода: 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);
}

Покрытие операторов измеряет процент операторов в вашем коде, которые выполняются вашими тестами. На первый взгляд вы можете задаться вопросом: «Разве это не то же самое, что покрытие линии?» Действительно, покрытие операторов похоже на покрытие строк, но учитывает отдельные строки кода, содержащие несколько операторов.

В примере кода имеется восемь строк исполняемого кода, но девять операторов. Можете ли вы найти строку, содержащую два утверждения?

Это следующая строка: espresso = 30 * cup; water = 70 * cup;

Тесты охватывают только пять утверждений из девяти, поэтому покрытие утверждений составляет 55,55%.

Если вы всегда пишете по одному оператору в каждой строке, охват вашей строки будет аналогичен охвату операторов.

Какой тип покрытия кода выбрать?

Большинство инструментов покрытия кода включают эти четыре типа общего покрытия кода. Выбор приоритетной метрики покрытия кода зависит от конкретных требований проекта, методов разработки и целей тестирования.

В целом покрытие операторов — хорошая отправная точка, поскольку это простой и понятный показатель. В отличие от покрытия операторов, покрытие ветвей и покрытие функций измеряют, вызывают ли тесты условие (ветвь) или функцию. Таким образом, они являются естественным развитием событий после освещения заявлений.

Как только вы достигнете высокого покрытия операторов, вы можете перейти к покрытию ветвей и функций.

Является ли тестовое покрытие тем же, что и покрытие кода?

Нет. Тестовое покрытие и покрытие кода часто путают, но они разные:

  • Тестовое покрытие : Качественный показатель, который измеряет, насколько хорошо набор тестов охватывает функции программного обеспечения. Это помогает определить уровень риска.
  • Покрытие кода : количественный показатель, измеряющий долю кода, выполненного во время тестирования. Речь идет о том, какой объем кода покрывают тесты.

Вот упрощенная аналогия: представьте себе веб-приложение как дом.

  • Покрытие тестами показывает, насколько хорошо тесты охватывают комнаты в доме.
  • Покрытие кода измеряет, какую часть дома прошли тесты.

100% покрытие кода не означает отсутствие ошибок

Конечно, желательно добиться высокого покрытия кода при тестировании, но 100%-ное покрытие кода не гарантирует отсутствие ошибок или недостатков в вашем коде.

Бессмысленный способ добиться 100% покрытия кода

Рассмотрим следующий тест:

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

Этот тест обеспечивает 100% покрытие функций, строк, ветвей и операторов, но он не имеет смысла, поскольку фактически не проверяет код. Утверждение expect(true).toBe(true) всегда будет проходить независимо от того, правильно ли работает код.

Плохая метрика хуже, чем отсутствие метрики

Плохая метрика может дать вам ложное чувство безопасности, что хуже, чем отсутствие метрики вообще. Например, если у вас есть набор тестов, обеспечивающий 100%-ное покрытие кода, но все тесты бессмысленны, у вас может возникнуть ложное ощущение безопасности, что ваш код хорошо протестирован. Если вы случайно удалите или сломаете часть кода приложения, тесты все равно пройдут, даже если приложение перестанет работать корректно.

Чтобы избежать этого сценария:

  • Тестовый обзор. Пишите и просматривайте тесты, чтобы убедиться в их значимости, и тестируйте код в различных сценариях.
  • Используйте покрытие кода в качестве ориентира , а не как единственный показатель эффективности тестирования или качества кода.

Использование покрытия кода в разных типах тестирования

Давайте подробнее рассмотрим, как можно использовать покрытие кода тремя распространенными типами тестов :

  • Юнит-тесты. Это лучший тип тестов для сбора данных о покрытии кода, поскольку они предназначены для охвата множества небольших сценариев и путей тестирования.
  • Интеграционные тесты. Они могут помочь собрать покрытие кода для интеграционных тестов, но используйте их с осторожностью. В этом случае вы рассчитываете покрытие большей части исходного кода, и может быть сложно определить, какие тесты действительно покрывают какие части кода. Тем не менее, расчет покрытия кода интеграционными тестами может быть полезен для устаревших систем, которые не имеют хорошо изолированных модулей.
  • Сквозные (E2E) тесты. Измерение покрытия кода для E2E-тестов является сложной задачей из-за сложной природы этих тестов. Вместо использования покрытия кода лучше использовать покрытие требований. Это связано с тем, что цель E2E-тестов — удовлетворить требования вашего теста, а не сосредоточиться на исходном коде.

Заключение

Покрытие кода может быть полезным показателем для измерения эффективности ваших тестов. Это может помочь вам улучшить качество вашего приложения, гарантируя, что важная логика вашего кода будет хорошо протестирована.

Однако помните, что покрытие кода — это всего лишь один показатель. Обязательно учитывайте и другие факторы, такие как качество ваших тестов и требования к вашему приложению.

Стремление к 100% покрытию кода не является целью. Вместо этого вам следует использовать покрытие кода вместе с хорошо продуманным планом тестирования, включающим различные методы тестирования, включая модульные тесты, интеграционные тесты, сквозные тесты и ручные тесты.

См. полный пример кода и тесты с хорошим покрытием кода. Вы также можете запустить код и тесты с помощью этой живой демонстрации .

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