Почему не нужно стремиться к 100% покрытию кода юнит-тестами

Бытует мнение, что 100% покрытие кода юнит-тестами обеспечивает безопасность и уверенность в его корректности. Это заблуждение: процент покрытия кода никак не связан с качеством набора тестов.

Рассмотрим простую функцию и её юнит-тест:

function isEmptyString(string) {
  if (string.length === 0) {
    return true;
  }

  return false;
}

it('works', () => {
  expect(isEmptyString('test')).toBe(false);
});

Такой тест покрывает 75% строк и 50% путей выполнения:

 PASS  ./index.spec.js
  ✓ works (4ms)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |       75 |       50 |      100 |       75 |                   |
 index.js |       75 |       50 |      100 |       75 |                 3 |
----------|----------|----------|----------|----------|-------------------|

Довольно посредственный результат. Давайте его улучшим:

function isEmptyString(string) {
  return string.length === 0;
}

Ух! Везде соточки:

 PASS  ./index.spec.js
  ✓ works (5ms)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 index.js |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|

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

Можно пойти дальше и, как полагается, порефакторить тесты:

it('works', () => {
  isEmptyString('test');
});

Результат снова потрясающий:

 PASS  ./index.spec.js
  ✓ works (2ms)

----------|----------|----------|----------|----------|-------------------|
File      |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
----------|----------|----------|----------|----------|-------------------|
All files |      100 |      100 |      100 |      100 |                   |
 index.js |      100 |      100 |      100 |      100 |                   |
----------|----------|----------|----------|----------|-------------------|

Мы выкинули единственную проверку из теста, сделав его, по сути, ещё более бесполезным, но покрытие так и осталось 100%.

Мораль

Покрытие кода работает хорошо как индикатор проблем: если оно низкое, вероятно, тестов написано слишком мало. Но использовать покрытие как индикатор качества тестов нельзя: высокого покрытия можно добиться даже без единой проверки.