Инициализация в конструкторе.
Тестовый класс инициализирует поля фикстуры в конструкторе вместо специального хука подготовки фреймворка (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
Перенесите инициализацию полей из конструктора в хук подготовки фреймворка и предпочитайте свежее состояние на каждый тест общим экземплярам.
- Удалите конструктор тестового класса; объявите поля и присвойте их в хуке подготовки (
@BeforeEach/setUp()для JUnit,[TestInitialize]для MSTest,beforeEachдля Jest/Vitest). - Если подготовка асинхронна, это обязательно — хук может выполнить
await; конструктор не может. - В JS/TS стройте фикстуру внутри
beforeEach(переприсваиваяlet), а не один раз на уровнеdescribe/модуля, чтобы каждый тест получал чистый объект. - Сохраняйте симметрию очистки: соединяйте
@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
- tsDetect (Test Smell Detector) Constructor Initialization
- MSTest Roslyn analyzers MSTEST0019