ConstructiCat Logo
CodeBust.
Browse section ▾

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 await dentro 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 final mais 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 (ou beforeEach/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.

  1. Exclua o construtor da classe de teste; declare os campos e atribua-os no hook de setup (@BeforeEach/setUp() para JUnit, [TestInitialize] para MSTest, beforeEach para Jest/Vitest).
  2. Se o setup for assíncrono, isso é obrigatório — o hook pode usar await; um construtor não pode.
  3. No JS/TS, construa a fixture dentro de beforeEach (reatribuindo um let) em vez de uma única vez no escopo de describe/módulo, para que cada teste receba um objeto limpo.
  4. Mantenha o teardown simétrico: combine @BeforeEach com @AfterEach (ou, no estilo de construtor do xUnit.net, implemente IDisposable).

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