Inicialização no Construtor.
Uma classe de teste inicializa seus campos de fixture em um construtor em vez de no hook de setup dedicado do framework (setUp / @BeforeEach / TestInitialize), contornando o ciclo de vida do teste.
##Signs and Symptoms
Uma classe de teste tem um construtor explícito que constrói o sistema sob teste, os mocks ou os campos compartilhados — trabalho que pertence ao hook de setup do framework (setUp() / @Before / @BeforeEach no JUnit, [TestInitialize] no MSTest, beforeEach no Jest/Vitest).
Sinais reveladores:
- A classe de teste declara um construtor que atribui a campos
final/de instância. - A configuração da fixture vive fora do ciclo de vida que o framework realmente gerencia — de modo que
@BeforeEach/@AfterEach, o setup da classe base e a reinicialização por teste são silenciosamente contornados ou executados em uma ordem confusa em relação ao construtor. - O setup assíncrono é impossível: você não pode usar
awaitdentro de um construtor, então o trabalho assíncrono de fixture acaba forçado para o lugar errado ou duplicado.
Forma canônica em JUnit (do catálogo de test smells):
public class TagEncodingTest extends BrambleTestCase {
private final CryptoComponent crypto;
private final SecretKey tagKey;
public TagEncodingTest() { // smell: inicialização no construtor
crypto = new CryptoComponentImpl(new TestSecureRandomProvider());
tagKey = TestUtils.getSecretKey();
}
}
Análogo em JS/TS — fixture construída uma única vez no escopo de describe/módulo (o equivalente ao construtor) em vez de por teste:
describe('Cart', () => {
const cart = new Cart(); // construído uma vez, vaza estado entre os testes
it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
it('starts empty', () => { expect(cart.size).toBe(0); }); // falha: enxerga apple
});
##Reasons for the Problem
Por que acontece
- Desenvolvedores não familiarizados com o propósito do hook de setup recorrem ao recurso da linguagem que já conhecem — o construtor — para inicializar os campos.
- Parece mais limpo: campos
finalmais um construtor soam idiomáticos, e uma IDE pode até gerar o construtor para você. - Cargo cult a partir de classes de produção, onde a inicialização no construtor é o padrão correto.
Por que é prejudicial
- Contorna o ciclo de vida do teste. Os frameworks prometem estado novo a cada teste por meio do hook de setup e de um par
@BeforeEach/@AfterEach(oubeforeEach/afterEach). A lógica escondida em um construtor fica fora desse contrato, de modo que a limpeza, o setup da classe base e as garantias de ordenação podem não se aplicar como esperado. - Vazamento de estado e instabilidade. Em frameworks que reutilizam uma única instância (ou quando você guarda a fixture em uma variável compartilhada/com escopo de
describe), as mutações de um teste vazam para o seguinte. Os testes passam ou falham dependendo da ordem de execução — falsa confiança e falhas intermitentes. - Sem setup assíncrono. Um construtor não pode usar
await. Qualquer setup que precise de I/O, de uma conexão de banco de dados ou de um servidor iniciado não consegue viver ali de forma limpa; as pessoas contornam isso com chamadas bloqueantes ou com código duplicado por teste. - Legibilidade/consistência. Revisores e ferramentas esperam as fixtures no hook convencional. Dividir a inicialização entre um construtor e um método de setup torna a fixture real difícil de encontrar e fácil de errar.
Ressalva — depende do framework. Esta não é uma regra universal. No xUnit.net, o construtor é o setup idiomático por teste (combinado com IDisposable para o teardown), e o MSTest até traz uma regra oposta, opcional (MSTEST0020), que prefere construtores em vez de [TestInitialize]. Trate a "Inicialização no Construtor" como um smell especificamente onde o seu framework fornece um hook de setup dedicado (JUnit, MSTest clássico, NUnit) e você está contornando-o.
##Treatment
Mova a inicialização dos campos para fora do construtor e para dentro do hook de setup do framework, e prefira um estado novo por teste em vez de instâncias compartilhadas.
- Exclua o construtor da classe de teste; declare os campos e atribua-os no hook de setup (
@BeforeEach/setUp()para JUnit,[TestInitialize]para MSTest,beforeEachpara Jest/Vitest). - Se o setup for assíncrono, isso é obrigatório — o hook pode usar
await; um construtor não pode. - No JS/TS, construa a fixture dentro de
beforeEach(reatribuindo umlet) em vez de uma única vez no escopo dedescribe/módulo, para que cada teste receba um objeto limpo. - Mantenha o teardown simétrico: combine
@BeforeEachcom@AfterEach(ou, no estilo de construtor do xUnit.net, implementeIDisposable).
Antes → depois (JUnit):
// antes
public class TagEncodingTest {
private final CryptoComponent crypto = new CryptoComponentImpl(...);
}
// depois
public class TagEncodingTest {
private CryptoComponent crypto;
@BeforeEach
void setUp() {
crypto = new CryptoComponentImpl(...);
}
}
Antes → depois (Jest/Vitest):
describe('Cart', () => {
let cart: Cart;
beforeEach(() => { cart = new Cart(); }); // novo a cada teste
it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
it('starts empty', () => { expect(cart.size).toBe(0); });
});
Se a fixture for genuinamente imutável e custosa (e o seu framework suportar isso), um hook de uma única vez, como @BeforeAll/beforeAll, é aceitável — mas apenas para estado compartilhado verdadeiramente somente leitura, nunca para objetos que os testes mutam.
##Detected by
- tsDetect (Test Smell Detector) Constructor Initialization
- MSTest Roslyn analyzers MSTEST0019