Неизвестный тест.
Тестовый метод, который выполняет код, но не содержит ни одной проверки, поэтому проходит, пока ничего не выбрасывает исключение, — а его подлинное назначение и то, что он проверяет, остаются неизвестными.
##Signs and Symptoms
Тест, который готовит объекты и вызывает тестируемую систему, а затем останавливается без единой проверки. Он зелёный просто потому, что не было выброшено исключение, — а не потому, что подтверждено какое-либо ожидаемое поведение. Ничто в теле не сообщает, как выглядит «правильно».
Характерные признаки:
- Нигде в теле теста нет
expect/assert/verify. - «Проверка» выполняется через
console.log/printрезультата, который человек должен оценить на глаз. - Расплывчатое имя теста (
testChainDependencies) и тело, не дающее ни намёка на то, что он гарантирует. - Тест остался бы зелёным, даже если бы продакшен-метод вернул совершенно неверное значение.
- Тесты, целиком опирающиеся на «оно не упало», не говоря об этом явно.
// Запах: выполняет код, печатает, ничего не проверяет — проходит независимо от того, что вернёт calculate()
test('chain dependencies', () => {
const game = Game.newGame(0, '');
game.setOtherGoods(Building.TOOLMAKERS, 1);
const logic = new Logic(game);
const res = logic.calculateChainWithDependencies(Goods.TOOLS);
console.log(res.toString()); // нет expect(...) — что вообще здесь проверяется?
});
Распространённый вариант прячется за обильным мокингом: тест настраивает моки и вызывает SUT, но так и не проверяет ни возвращаемое значение, ни взаимодействия с моками.
##Reasons for the Problem
Почему это происходит
- Был создан заготовочный/TODO-тест ("лишь бы компилировался и запускался"), а проверку так и не дописали.
- Отладочные леса —
console.log, ручной прогон «на глазок» — попали в коммит так, будто это полноценный тест. - При рефакторинге проверку удалили или закомментировали, но оставили подготовку данных.
- «Запускается без исключений» принимают за «работает». Дымовые тесты вполне законны, но здесь намерение сделать дымовой тест нигде не выражено явно.
- Автоматически или ИИ-сгенерированные заготовки тестов, которые вызывают метод, но не проверяют результат.
Чем это вредит
- Ложная уверенность. Тест зеленеет в отчёте и добавляет покрытие строк/ветвей, но не проверяет ничего. Метрики покрытия откровенно лгут о том, насколько код защищён.
- Нет защиты от регрессий. Поведение может незаметно сломаться — неверное возвращаемое значение, неверное состояние — а набор тестов останется зелёным. Это худший вид теста: он требует сопровождения, но ничего не ловит.
- Неизвестное намерение. Читатель (или будущий сопровождающий) не может понять, какое поведение гарантируется, а значит, не может безопасно менять ни код, ни тест. Тест ничего не документирует.
- Подрывает доверие к набору тестов. Стоит людям заметить тесты, которые на самом деле ничего не тестируют, и они перестают верить, что зелёный значит хорошо, — а это обесценивает и все остальные тесты.
Это зеркальное отражение рулетки проверок: там слишком много недокументированных проверок, а в Неизвестном тесте их нет вообще.
##Treatment
Пусть каждый тест явно заявляет, чего он ожидает, и пусть «он просто не должен выбрасывать исключение» будет осознанным, явным выбором.
- Добавьте хотя бы одну проверку наблюдаемого результата — возвращаемого значения, итогового состояния или выброшенного исключения. Замените отладочные
console.log/print наexpectдля этого значения. - Если настоящее намерение — «это не должно выбрасывать исключение», скажите об этом явно через
expect(() => fn()).not.toThrow()(илиawait expect(fn()).resolves.toBeDefined()). Теперь дымовой тест задокументирован, а не случаен. - Для тестов, проверяющих только взаимодействия, проверяйте коллаборатора:
expect(mock).toHaveBeenCalledWith(...). - Удаляйте или помечайте
skip/todoмёртвые заготовки вместо того, чтобы оставлять пустой зелёный тест (it.todo('handles chained deps')честно фиксирует пробел, не подделывая покрытие). - Включите детектор (
jest/expect-expect,vitest/expect-expectили SonarSource S2699) в CI. Если вы оборачиваете проверки в собственные вспомогательные функции, зарегистрируйте их через опцию правилаassertFunctionNames, чтобы настоящие проверки не помечались как нарушения.
// До — Неизвестный тест
test('chain dependencies', () => {
const logic = new Logic(Game.newGame(0, ''));
const res = logic.calculateChainWithDependencies(Goods.TOOLS);
console.log(res.toString());
});
// После — намерение и гарантия выражены явно
test('resolves tools to the toolmakers workshop chain', () => {
const logic = new Logic(Game.newGame(0, ''));
const res = logic.calculateChainWithDependencies(Goods.TOOLS);
expect(res).toHaveLength(1);
expect(res[0].building).toBe(Building.TOOLMAKERS);
});
##Detected by
- eslint-jest expect-expect — jest/expect-expect
- eslint-vitest expect-expect — vitest/expect-expect
- sonar S2699 — Тесты должны содержать проверки (JavaScript)
- sonar S2699 — Тесты должны содержать проверки (Java)