ConstructiCat Logo
CodeBust.
Browse section ▾

Inicialización en el constructor.

Una clase de test inicializa los campos de su fixture en un constructor en lugar de en el hook de configuración dedicado del framework (setUp / @BeforeEach / TestInitialize), saltándose el ciclo de vida del test.

##Signs and Symptoms

Una clase de test tiene un constructor explícito que construye el sistema bajo prueba, los mocks o los campos compartidos —trabajo que corresponde al hook de configuración del framework (setUp() / @Before / @BeforeEach en JUnit, [TestInitialize] en MSTest, beforeEach en Jest/Vitest)—.

Señales reveladoras:

  • La clase de test declara un constructor que asigna a campos final/de instancia.
  • La configuración del fixture vive fuera del ciclo de vida que el framework gestiona realmente, de modo que @BeforeEach/@AfterEach, la configuración de la clase base y la reinicialización por test se saltan silenciosamente o se ejecutan en un orden confuso respecto al constructor.
  • La configuración asíncrona es imposible: no puedes usar await dentro de un constructor, así que el trabajo de fixture asíncrono se ve forzado al lugar equivocado o se duplica.

Forma canónica en JUnit (del catálogo de test smells):

public class TagEncodingTest extends BrambleTestCase {
    private final CryptoComponent crypto;
    private final SecretKey tagKey;

    public TagEncodingTest() {                 // mal olor: inicialización en el constructor
        crypto = new CryptoComponentImpl(new TestSecureRandomProvider());
        tagKey = TestUtils.getSecretKey();
    }
}

Análogo en JS/TS: el fixture se construye una sola vez en el ámbito de describe/módulo (el equivalente al constructor) en lugar de por test:

describe('Cart', () => {
  const cart = new Cart();          // se construye una vez, filtra estado entre tests
  it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
  it('starts empty', () => { expect(cart.size).toBe(0); }); // falla: ve apple
});

##Reasons for the Problem

Por qué ocurre

  • Las personas desarrolladoras que no conocen el propósito del hook de configuración recurren a la característica del lenguaje que ya conocen —el constructor— para inicializar los campos.
  • Parece más limpio: los campos final junto con un constructor resultan idiomáticos, e incluso un IDE puede generar el constructor por ti.
  • Imitación acrítica (cargo cult) de las clases de producción, donde la inicialización en el constructor es el patrón correcto.

Por qué es perjudicial

  • Se salta el ciclo de vida del test. Los frameworks prometen un estado fresco por test mediante el hook de configuración y un par @BeforeEach/@AfterEach (o beforeEach/afterEach). La lógica oculta en un constructor queda fuera de ese contrato, así que la limpieza, la configuración de la clase base y las garantías de orden pueden no aplicarse como se espera.
  • Fuga de estado e inestabilidad. En los frameworks que reutilizan una sola instancia (o cuando guardas el fixture en una variable compartida o con ámbito de describe), las mutaciones de un test se filtran al siguiente. Los tests pasan o fallan según el orden de ejecución: falsa confianza y fallos intermitentes.
  • Sin configuración asíncrona. Un constructor no puede usar await. Cualquier configuración que necesite E/S, una conexión a BD o un servidor arrancado no puede vivir ahí de forma limpia; la gente lo sortea con llamadas bloqueantes o con código duplicado por test.
  • Legibilidad/consistencia. Los revisores y las herramientas esperan los fixtures en el hook convencional. Repartir la inicialización entre un constructor y un método de configuración hace que el fixture real sea difícil de encontrar y fácil de equivocar.

Advertencia: depende del framework. Esto no es una regla universal. En xUnit.net el constructor es la configuración idiomática por test (emparejado con IDisposable para el teardown), y MSTest incluso incluye una regla opuesta y opcional (MSTEST0020) que prefiere los constructores frente a [TestInitialize]. Considera «Constructor Initialization» un mal olor específicamente cuando tu framework proporciona un hook de configuración dedicado (JUnit, MSTest clásico, NUnit) y lo estás evitando.

##Treatment

Saca la inicialización de campos del constructor y llévala al hook de configuración del framework, y prefiere un estado fresco por test antes que instancias compartidas.

  1. Elimina el constructor de la clase de test; declara los campos y asígnalos en el hook de configuración (@BeforeEach/setUp() para JUnit, [TestInitialize] para MSTest, beforeEach para Jest/Vitest).
  2. Si la configuración es asíncrona, esto es obligatorio: el hook puede usar await; un constructor no.
  3. En JS/TS, construye el fixture dentro de beforeEach (reasignando un let) en lugar de una sola vez en el ámbito de describe/módulo, de modo que cada test reciba un objeto limpio.
  4. Mantén el teardown simétrico: empareja @BeforeEach con @AfterEach (o, en el estilo de constructor de xUnit.net, implementa IDisposable).

Antes → después (JUnit):

// antes
public class TagEncodingTest {
    private final CryptoComponent crypto = new CryptoComponentImpl(...);
}

// después
public class TagEncodingTest {
    private CryptoComponent crypto;

    @BeforeEach
    void setUp() {
        crypto = new CryptoComponentImpl(...);
    }
}

Antes → después (Jest/Vitest):

describe('Cart', () => {
  let cart: Cart;
  beforeEach(() => { cart = new Cart(); }); // fresco por test
  it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
  it('starts empty', () => { expect(cart.size).toBe(0); });
});

Si el fixture es genuinamente inmutable y costoso (y tu framework lo admite), un hook de una sola vez como @BeforeAll/beforeAll es aceptable, pero solo para estado compartido verdaderamente de solo lectura, nunca para objetos que los tests mutan.

##Detected by