ConstructiCat Logo
CodeBust.
Browse section ▾

Инициализация в конструкторе.

Тестовый класс инициализирует поля фикстуры в конструкторе вместо специального хука подготовки фреймворка (setUp / @BeforeEach / TestInitialize), обходя жизненный цикл теста.

##Signs and Symptoms

Тестовый класс имеет явный конструктор, который строит систему под тестом, моки или общие поля — работа, которая должна жить в хуке подготовки фреймворка (setUp() / @Before / @BeforeEach в JUnit, [TestInitialize] в MSTest, beforeEach в Jest/Vitest).

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

  • Тестовый класс объявляет конструктор, присваивающий значения final-полям/полям экземпляра.
  • Подготовка фикстуры живёт вне жизненного цикла, которым реально управляет фреймворк, — поэтому @BeforeEach/@AfterEach, подготовка базового класса и переинициализация на каждый тест молча обходятся или выполняются в запутанном порядке относительно конструктора.
  • Асинхронная подготовка невозможна: внутри конструктора нельзя выполнить await, поэтому асинхронная работа по фикстуре вынужденно попадает не туда или дублируется.

Каноническая форма для JUnit (из каталога тестовых запахов):

public class TagEncodingTest extends BrambleTestCase {
    private final CryptoComponent crypto;
    private final SecretKey tagKey;

    public TagEncodingTest() {                 // запах: инициализация в конструкторе
        crypto = new CryptoComponentImpl(new TestSecureRandomProvider());
        tagKey = TestUtils.getSecretKey();
    }
}

Аналог на JS/TS — фикстура построена один раз на уровне describe/модуля (эквивалент конструктора) вместо построения на каждый тест:

describe('Cart', () => {
  const cart = new Cart();          // построена один раз, протекает состоянием между тестами
  it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
  it('starts empty', () => { expect(cart.size).toBe(0); }); // падает: видит apple
});

##Reasons for the Problem

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

  • Разработчики, незнакомые с назначением хука подготовки, хватаются за известную им языковую возможность — конструктор — чтобы инициализировать поля.
  • Так выглядит чище: final-поля плюс конструктор кажутся идиоматичными, а IDE может даже сгенерировать конструктор за вас.
  • Карго-культ из боевых классов, где инициализация в конструкторе и есть правильный приём.

Почему это вредно

  • Обходит жизненный цикл теста. Фреймворки обещают свежее состояние на каждый тест через хук подготовки и пару @BeforeEach/@AfterEach (или beforeEach/afterEach). Логика, спрятанная в конструкторе, находится вне этого контракта, поэтому очистка, подготовка базового класса и гарантии порядка могут не сработать так, как ожидается.
  • Утечка состояния и нестабильность. Во фреймворках, переиспользующих один экземпляр (или когда вы прячете фикстуру в общую переменную/переменную уровня describe), мутации одного теста просачиваются в следующий. Тесты проходят или падают в зависимости от порядка выполнения — ложная уверенность и периодические отказы.
  • Нет асинхронной подготовки. Конструктор не может выполнять await. Любая подготовка, которой нужны ввод-вывод, соединение с БД или запущенный сервер, не может чисто жить там; люди обходят это блокирующими вызовами или дублированием кода в каждом тесте.
  • Читаемость/единообразие. Ревьюеры и инструменты ожидают фикстуры в общепринятом хуке. Разделение инициализации между конструктором и методом подготовки затрудняет поиск реальной фикстуры и облегчает ошибки.

Оговорка — это зависит от фреймворка. Это не универсальное правило. В xUnit.net конструктор и есть идиоматичная подготовка на каждый тест (в паре с IDisposable для очистки), а MSTest даже поставляет противоположное, включаемое по выбору правило (MSTEST0020), предпочитающее конструкторы вместо [TestInitialize]. Считайте «инициализацию в конструкторе» запахом именно там, где ваш фреймворк предоставляет специальный хук подготовки (JUnit, классический MSTest, NUnit), а вы его обходите.

##Treatment

Перенесите инициализацию полей из конструктора в хук подготовки фреймворка и предпочитайте свежее состояние на каждый тест общим экземплярам.

  1. Удалите конструктор тестового класса; объявите поля и присвойте их в хуке подготовки (@BeforeEach/setUp() для JUnit, [TestInitialize] для MSTest, beforeEach для Jest/Vitest).
  2. Если подготовка асинхронна, это обязательно — хук может выполнить await; конструктор не может.
  3. В JS/TS стройте фикстуру внутри beforeEach (переприсваивая let), а не один раз на уровне describe/модуля, чтобы каждый тест получал чистый объект.
  4. Сохраняйте симметрию очистки: соединяйте @BeforeEach с @AfterEach (или, в стиле конструкторов xUnit.net, реализуйте IDisposable).

До → после (JUnit):

// до
public class TagEncodingTest {
    private final CryptoComponent crypto = new CryptoComponentImpl(...);
}

// после
public class TagEncodingTest {
    private CryptoComponent crypto;

    @BeforeEach
    void setUp() {
        crypto = new CryptoComponentImpl(...);
    }
}

До → после (Jest/Vitest):

describe('Cart', () => {
  let cart: Cart;
  beforeEach(() => { cart = new Cart(); }); // свежая на каждый тест
  it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
  it('starts empty', () => { expect(cart.size).toBe(0); });
});

Если фикстура действительно неизменяема и дорога (и ваш фреймворк это поддерживает), одноразовый хук вроде @BeforeAll/beforeAll приемлем — но только для по-настоящему доступного только для чтения общего состояния, никогда для объектов, которые тесты изменяют.

##Detected by