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
awaitdentro 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
finaljunto 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 sí 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(obeforeEach/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.
- 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,beforeEachpara Jest/Vitest). - Si la configuración es asíncrona, esto es obligatorio: el hook puede usar
await; un constructor no. - En JS/TS, construye el fixture dentro de
beforeEach(reasignando unlet) en lugar de una sola vez en el ámbito dedescribe/módulo, de modo que cada test reciba un objeto limpio. - Mantén el teardown simétrico: empareja
@BeforeEachcon@AfterEach(o, en el estilo de constructor de xUnit.net, implementaIDisposable).
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
- tsDetect (Test Smell Detector) Constructor Initialization
- MSTest Roslyn analyzers MSTEST0019