Ресурсный оптимизм.
Ресурсный оптимизм — это когда тест предполагает, что внешний ресурс (файл, каталог, таблица БД, переменная окружения или сетевой эндпоинт) уже существует и находится в известном состоянии, вместо того чтобы создать и проверить его, из-за чего тест проходит или падает недетерминированно.
##Signs and Symptoms
Тест трогает нечто вне себя — файл, временный путь, каталог, строку базы данных, переменную окружения или удалённый эндпоинт — и просто верит, что оно уже на месте и правильной формы. Нет ни шага подготовки, который создавал бы ресурс, ни проверки, что он существует, перед его использованием.
Характерные признаки:
- По жёстко зашитому пути читают или пишут без предварительного
existsSync/mkdir/writeFile: например,fs.readFileSync('/tmp/app/config.json'). - Тест проходит на вашей машине или при первом прогоне, а затем падает на чистом чекауте, в CI, в другом системном каталоге временных файлов или когда набор тестов выполняется в ином порядке либо параллельно.
- Нужный ему ресурс на самом деле создаётся другим тестом (или ручным/только-для-разработки шагом), поэтому тест работает лишь как побочный эффект порядка выполнения.
- Он открывает файл/соединение и проверяет результат, ни разу не проверив наличие ресурса, — отсутствующий ресурс выбрасывает исключение до настоящей проверки или незаметно отдаёт пустые данные, которые всё равно «проходят».
test('parses the config file', () => {
// оптимистично: предполагает, что /tmp/app/config.json уже существует и корректно сформирован
const raw = fs.readFileSync('/tmp/app/config.json', 'utf8');
expect(JSON.parse(raw).port).toBe(8080);
});
Каноническая эвристика обнаружения (testsmells.org / tsDetect): тест использует ресурс типа File, не вызвав сначала проверку существования/корректности вроде exists(), isFile() или notExists().
##Reasons for the Problem
Почему это происходит
- Ресурс присутствовал, пока тест писали (фикстурный файл в репозитории, засеянная dev-база, временный файл, оставленный предыдущим шагом), поэтому автор никогда не ощущает его отсутствия.
- Указать на существующий путь или таблицу — это просто меньше кода, чем выделять и засевать их в подготовке, а затем убирать за собой.
- Копипаст из другого теста, который уже опирался на общее, фоновое состояние.
Чем это вредит
- Недетерминизм / флакование. Результат зависит от состояния окружения, а не от тестируемого кода. ван Дёрсен и др. описывают ровно это: тесты, которые «в один момент работают отлично, а в другой — с треском падают». Чистые CI-раннеры, свежие чекауты, параллельные воркеры и разные системные каталоги для временных файлов — вот где это бьёт.
- Ложная уверенность. Зелёный тест может проходить из-за остаточного состояния от предыдущего прогона или предыдущего теста, а не потому, что текущий код корректен, — либо отсутствующий ресурс рано выбрасывает исключение, и значимая проверка вообще не выполняется.
- Скрытая связанность и зависимость от порядка. Когда один тест создаёт то, что потребляет другой, у набора тестов появляется невидимый контракт на порядок выполнения, который ломается при перемешивании или шардировании.
- Трудно воспроизвести и сопровождать. Падения нельзя воспроизвести локально, потому что они зависят от характерного для конкретной машины фонового состояния, так что отладка идёт медленно, а тест подрывает доверие.
##Treatment
Сделайте так, чтобы каждый тест владел и управлял любым ресурсом, которого он касается, и никогда не полагался на фоновое состояние.
- Готовьте в подготовке, прибирайте в завершении. Используйте Setup External Resource — выделяйте и инициализируйте файлы, каталоги, таблицы БД и соединения в
beforeEach/beforeAll, а освобождайте их вafterEach/afterAll, чтобы следующий прогон начинался с чистого листа. - Создавайте, а не предполагайте. Запишите файл, засейте таблицу или поднимите стаб-сервер в подготовке. Если вам действительно необходимо использовать уже существующий ресурс, сначала проверьте, что он есть, чтобы отсутствующий ресурс падал громко и с внятным сообщением, а не портил настоящую проверку.
- Изолируйте на каждый тест. Используйте уникальный временный каталог (
fs.mkdtemp(os.tmpdir() + …)) или свежую схему/пространство имён на каждый тест вместо общего жёстко зашитого пути, чтобы параллельные прогоны и перезапуски не сталкивались. - А ещё лучше — уберите зависимость. Замените реальный ресурс моком или in-memory фейком (замоканный
fs, БД в памяти, заглушённый HTTP), чтобы тест был полностью самодостаточным и детерминированным, — именно такое исправление рекомендует testsmells.org.
До → после:
// до — ресурсный оптимизм
test('parses the config file', () => {
const raw = fs.readFileSync('/tmp/app/config.json', 'utf8');
expect(JSON.parse(raw).port).toBe(8080);
});
// после — тест владеет ресурсом и проверяет его
import { mkdtemp, writeFile, rm, readFile } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
let dir;
beforeEach(async () => {
dir = await mkdtemp(path.join(os.tmpdir(), 'cfg-'));
await writeFile(path.join(dir, 'config.json'), JSON.stringify({ port: 8080 }));
});
afterEach(() => rm(dir, { recursive: true, force: true }));
test('parses the config file', async () => {
const raw = await readFile(path.join(dir, 'config.json'), 'utf8');
expect(JSON.parse(raw).port).toBe(8080);
});
##Detected by
- tsDetect Resource Optimism — Resource Optimism