Logotipo do ConstructiCat
CodeBust.
Explorar a seção ▾

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/@AfterEach e 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 com PER_CLASS), os campos não são reinicializados antes de cada teste.
  • 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();
    }
}

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 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 — 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.

  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. 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 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(...);
    }
}

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)