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.
##Sinais e sintomas
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 os hooks
@BeforeEach/@AfterEache o setup da classe base rodam em uma ordem confusa em relação ao construtor, e, em frameworks que reutilizam a instância (NUnit, JUnit 5 comPER_CLASS), os campos não são reinicializados antes de cada teste. - 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();
}
}
Relacionado, mas não é o mesmo smell: arquivos de teste em JS/TS não têm construtor de classe de teste, mas construir a fixture uma única vez no escopo de describe/módulo causa o vazamento de estado muitas vezes atribuído aos construtores. Trata-se de uma fixture compartilhada (veja General Fixture) que faz os testes dependerem da ordem de execução (Dependência de Ordem dos Testes):
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
});
##Causas do problema
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 — onde a instância é reutilizada. O JUnit (3, 4 e 5 no ciclo de vida padrão, por método), o MSTest e o xUnit.net criam uma nova instância da classe de teste para cada método de teste, então ali o construtor roda antes de cada teste e não carrega estado de um teste para outro. Já o NUnit, por padrão, reutiliza uma única instância da fixture para todos os testes da classe, assim como o JUnit 5 com
@TestInstance(Lifecycle.PER_CLASS): os campos mutáveis inicializados no construtor vazam de um teste para o seguinte, e o resultado passa a depender da ordem de execução. - 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.
##Tratamento
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. - Para o problema relacionado de fixture compartilhada em JS/TS (veja General Fixture / Dependência de Ordem dos Testes), 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(...);
}
}
Caso relacionado (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.
##Detectado por
- tsDetect Constructor Initialization — Sinaliza classes de teste que declaram um construtor (o smell Constructor Initialization do catálogo do tsDetect)
- mstest-analyzers MSTEST0019 — Prefira métodos TestInitialize a construtores (analisador do MSTest que precisa ser ativado)