---
title: "Таинственный гость"
type: "test-smell"
slug: "mystery-guest"
url: "http://localhost:3000/ru/test-smells/mystery-guest.md"
category: "Запахи фикстур"
description: "Тест, чьи входные данные или ожидаемые результаты находятся во внешнем ресурсе — файле, сидовых данных БД или общей фикстуре, — поэтому понять такой тест и доверять ему, читая только его, невозможно."
---
# Таинственный гость

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

## Signs and Symptoms

Тест невозможно понять, просто прочитав его: данные, которые им управляют (и обосновывают его проверки), находятся где-то за кадром — в файле CSV/JSON, сид-скрипте БД, общем модуле фикстур или аннотации фреймворка с данными-фикстурами. Тест ссылается на непрозрачный ресурс, а затем проверяет «магические» значения, смысл которых спрятан в этом ресурсе.

Характерные признаки:

* В теле вызывается что-то вроде `readFileSync('fixtures/subscribers.csv')`, `loadSeed('invoices.sql')`, `getResource(...)` или глобальный `beforeAll`, который засевает общую базу данных.
* Ожидаемые значения выглядят произвольными (`toHaveLength(3)`, конкретный id/email), и приходится открывать другой файл, чтобы узнать, _почему_.
* Общая/«универсальная» фикстура переиспользуется во многих тестах, поэтому входные данные, важные для _этого_ теста, погребены среди данных, которые ему безразличны.
* Тесты ломаются, когда коллега правит общую фикстуру ради не связанного с ними теста.

```js
test('returns active subscribers', async () => {
  // Таинственный гость: какие строки в этом файле? почему 3 — это правильно?
  const subscribers = await loadSubscribersFromCsv('./fixtures/subscribers.csv');
  const result = filterActive(subscribers);
  expect(result).toHaveLength(3); // обоснование живёт вне теста
});

```

Исходный пример из каталога Месароша / Test Smells имеет ту же форму: `loadAirportsAndFlightsFromFile("test-flights.csv")`, за которым следует `assertEquals(1, flightsAtOrigin.size())`, — это «1» обретает смысл, только если прочитать `test-flights.csv`.

## Reasons for the Problem

**Почему это происходит**

* _DRY, доведённый до крайности._ Тестовые данные выносят в общий файл или «универсальную фикстуру», чтобы избежать дублирования, разменивая читаемость на переиспользование.
* _Удобство работы с реальными данными._ Сбросить продакшен-CSV/JSON или `seed.sql` кажется проще, чем конструировать объекты прямо в тесте.
* _Легаси/интеграционная подготовка._ Наборы тестов, которые поднимают общую базу данных или опираются на аннотации фреймворка с данными-фикстурами (`@magentoDataFixture …`), по умолчанию наследуют скрытое состояние.

**Чем это вредит**

* _Читаемость / причинно-следственная связь._ Связь между входом и ожидаемым выходом разорвана. Читатель не может понять, _почему_ проверка верна, не покидая теста, а это сводит на нет роль теста как исполняемой документации.
* _Ложная уверенность._ Вы на самом деле не знаете, что именно проверяет тест; файл может содержать больше (или меньше), чем вы предполагаете, поэтому зелёный прогон доказывает меньше, чем кажется.
* _Надёжность / детерминизм._ Внешний ресурс может отсутствовать, быть переименован, переформатирован или различаться по окружению, ОС, кодировке или локали — и тогда падения отражают состояние _фикстуры_, а не реальный дефект (флакающие тесты).
* _Сопровождаемость / связанность._ Когда несколько тестов делят один ресурс, любой, кто правит его ради одного теста, может незаметно сломать остальные, и никто не знает, от каких полей зависит каждый тест.

## Treatment

Сделайте _значимый_ вход видимым внутри теста, рядом с зависящей от него проверкой (**свежая фикстура** / встроенная подготовка). Цель в том, чтобы ожидаемое значение становилось самоочевидным.

Конкретные шаги:

1. **Внесите значимые данные прямо в тест.** Сконструируйте в теле теста те несколько объектов/строк, которые важны тесту, чтобы ожидаемое значение проверки было очевидно верным.
2. **Если файл действительно нужен, постройте его в тесте.** Используйте помощник, который принимает только существенные параметры и пишет во временный путь, а затем прибирает за собой, — так значимые значения окажутся в тесте, а не в закоммиченном коме.
3. **Замените общие/«универсальные» фикстуры фикстурами на каждый тест** или предоставьте создатели/искатели, раскрывающие намерение (`createProductWithName('Simple Product')`, `getRecentlyAddedProduct()`), чтобы проверяемые атрибуты были явными, а несущественная подготовка пряталась за хорошо названным билдером, а не за непрозрачным ресурсом.

До → после:

```js
// До — Таинственный гость: данные и "2" спрятаны в сид-файле
test('flags overdue invoices', async () => {
  await seedDatabaseFromFixture('invoices.sql');
  const overdue = await findOverdueInvoices();
  expect(overdue).toHaveLength(2);
});

// После — входы видны; ожидаемый результат очевиден
test('flags overdue invoices', async () => {
  await insertInvoice({ id: 1, dueDate: '2020-01-01', paid: false }); // просрочен
  await insertInvoice({ id: 2, dueDate: '2099-01-01', paid: false }); // срок ещё не наступил
  const overdue = await findOverdueInvoices();
  expect(overdue.map(i => i.id)).toEqual([1]);
});

```

Для случая с файлом предпочтите узкоспециализированный помощник закоммиченному файлу:

```js
const csv = makeSubscriberCsv(tmpFile, 'active@x.com', 'active@y.com', 'active@z.com');
// теперь "3 активных" обосновано тем, что видно в тесте, а не скрытым файлом

```
