Инициализация в конструкторе.
Тестовый класс инициализирует поля фикстуры в конструкторе вместо специального хука подготовки фреймворка (setUp / @BeforeEach / TestInitialize), обходя жизненный цикл теста.
##Признаки и симптомы
Тестовый класс имеет явный конструктор, который строит систему под тестом, моки или общие поля — работа, которая должна жить в хуке подготовки фреймворка (setUp() / @Before / @BeforeEach в JUnit, [TestInitialize] в MSTest, beforeEach в Jest/Vitest).
Характерные признаки:
- Тестовый класс объявляет конструктор, присваивающий значения
final-полям/полям экземпляра. - Подготовка фикстуры живёт вне жизненного цикла, которым реально управляет фреймворк, — поэтому хуки
@BeforeEach/@AfterEachи подготовка базового класса выполняются в запутанном порядке относительно конструктора, а во фреймворках, переиспользующих экземпляр (NUnit, JUnit 5 сPER_CLASS), поля не переинициализируются перед каждым тестом. - Асинхронная подготовка невозможна: внутри конструктора нельзя выполнить
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
});
##Причины проблемы
Почему это происходит
- Разработчики, незнакомые с назначением хука подготовки, хватаются за известную им языковую возможность — конструктор — чтобы инициализировать поля.
- Так выглядит чище:
final-поля плюс конструктор кажутся идиоматичными, а IDE может даже сгенерировать конструктор за вас. - Карго-культ из боевых классов, где инициализация в конструкторе и есть правильный приём.
Почему это вредно
- Обходит жизненный цикл теста. Фреймворки обещают свежее состояние на каждый тест через хук подготовки и пару
@BeforeEach/@AfterEach(илиbeforeEach/afterEach). Логика, спрятанная в конструкторе, находится вне этого контракта, поэтому очистка, подготовка базового класса и гарантии порядка могут не сработать так, как ожидается. - Утечка состояния — там, где экземпляр переиспользуется. JUnit (3, 4 и 5 с жизненным циклом по умолчанию — экземпляр на метод), MSTest и xUnit.net создают новый экземпляр тестового класса для каждого тестового метода, поэтому конструктор там выполняется перед каждым тестом и не переносит состояние из одного теста в другой. А вот NUnit по умолчанию переиспользует один экземпляр фикстуры для всех тестов класса — как и JUnit 5 с
@TestInstance(Lifecycle.PER_CLASS): изменяемые поля, инициализированные в конструкторе, перетекают из теста в тест, и результат зависит от порядка выполнения. - Нет асинхронной подготовки. Конструктор не может выполнять
await. Любая подготовка, которой нужны ввод-вывод, соединение с БД или запущенный сервер, не может чисто жить там; люди обходят это блокирующими вызовами или дублированием кода в каждом тесте. - Читаемость/единообразие. Ревьюеры и инструменты ожидают фикстуры в общепринятом хуке. Разделение инициализации между конструктором и методом подготовки затрудняет поиск реальной фикстуры и облегчает ошибки.
Оговорка — это зависит от фреймворка. Это не универсальное правило. В xUnit.net конструктор и есть идиоматичная подготовка на каждый тест (в паре с IDisposable для очистки), а MSTest даже поставляет противоположное, включаемое по выбору правило (MSTEST0020), предпочитающее конструкторы вместо [TestInitialize]. Считайте «инициализацию в конструкторе» запахом именно там, где ваш фреймворк предоставляет специальный хук подготовки (JUnit, классический MSTest, NUnit), а вы его обходите.
##Лечение
Перенесите инициализацию полей из конструктора в хук подготовки фреймворка и предпочитайте свежее состояние на каждый тест общим экземплярам.
- Удалите конструктор тестового класса; объявите поля и присвойте их в хуке подготовки (
@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 приемлем — но только для по-настоящему доступного только для чтения общего состояния, никогда для объектов, которые тесты изменяют.
##Обнаружение анализаторами
- tsDetect Constructor Initialization — Помечает тестовые классы, объявляющие конструктор (запах «Инициализация в конструкторе» в каталоге tsDetect)
- mstest-analyzers MSTEST0019 — Предпочитайте методы TestInitialize конструкторам (необязательный анализатор MSTest, включается вручную)