ConstructiCat Logo
CodeBust.
Browse section ▾

Ресурсный оптимизм.

Ресурсный оптимизм — это когда тест предполагает, что внешний ресурс (файл, каталог, таблица БД, переменная окружения или сетевой эндпоинт) уже существует и находится в известном состоянии, вместо того чтобы создать и проверить его, из-за чего тест проходит или падает недетерминированно.

##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

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

  1. Готовьте в подготовке, прибирайте в завершении. Используйте Setup External Resource — выделяйте и инициализируйте файлы, каталоги, таблицы БД и соединения в beforeEach/beforeAll, а освобождайте их в afterEach/afterAll, чтобы следующий прогон начинался с чистого листа.
  2. Создавайте, а не предполагайте. Запишите файл, засейте таблицу или поднимите стаб-сервер в подготовке. Если вам действительно необходимо использовать уже существующий ресурс, сначала проверьте, что он есть, чтобы отсутствующий ресурс падал громко и с внятным сообщением, а не портил настоящую проверку.
  3. Изолируйте на каждый тест. Используйте уникальный временный каталог (fs.mkdtemp(os.tmpdir() + …)) или свежую схему/пространство имён на каждый тест вместо общего жёстко зашитого пути, чтобы параллельные прогоны и перезапуски не сталкивались.
  4. А ещё лучше — уберите зависимость. Замените реальный ресурс моком или 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