Constructor Initialization.
A test class initializes its fixture fields in a constructor instead of the framework's dedicated setup hook (setUp / @BeforeEach / TestInitialize), bypassing the test lifecycle.
##Signs and Symptoms
A test class has an explicit constructor that builds the system under test, mocks, or shared fields — work that belongs in the framework's setup hook (setUp() / @Before / @BeforeEach in JUnit, [TestInitialize] in MSTest, beforeEach in Jest/Vitest).
Tell-tale signs:
- The test class declares a constructor that assigns to
final/instance fields. - Fixture setup lives outside the lifecycle the framework actually manages — so
@BeforeEach/@AfterEachhooks and base-class setup run in a confusing order relative to the constructor, and in frameworks that reuse the instance (NUnit, JUnit 5 withPER_CLASS) the fields are not re-initialized before each test. - Asynchronous setup is impossible: you cannot
awaitinside a constructor, so async fixture work gets forced into the wrong place or duplicated.
Canonical JUnit form (from the test-smells catalog):
public class TagEncodingTest extends BrambleTestCase {
private final CryptoComponent crypto;
private final SecretKey tagKey;
public TagEncodingTest() { // smell: init in constructor
crypto = new CryptoComponentImpl(new TestSecureRandomProvider());
tagKey = TestUtils.getSecretKey();
}
}
Related, but not the same smell — JS/TS test files have no test-class constructor, yet building the fixture once at describe/module scope produces the state leakage often blamed on constructors. That is a shared fixture (see General Fixture) that makes tests depend on execution order (Test Order Dependency):
describe('Cart', () => {
const cart = new Cart(); // built once, leaks state across tests
it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
it('starts empty', () => { expect(cart.size).toBe(0); }); // fails: sees apple
});
##Reasons for the Problem
Why it happens
- Developers unfamiliar with the purpose of the setup hook reach for the language feature they already know — the constructor — to initialize fields.
- It looks cleaner:
finalfields plus a constructor feel idiomatic, and an IDE may even generate the constructor for you. - Cargo-culting from production classes, where constructor initialization is the right pattern.
Why it hurts
- Bypasses the test lifecycle. Frameworks promise fresh state per test via the setup hook and an
@BeforeEach/@AfterEach(orbeforeEach/afterEach) pair. Logic hidden in a constructor sits outside that contract, so cleanup, base-class setup, and ordering guarantees may not apply as expected. - State leakage — where the instance is reused. JUnit (3, 4, and 5 in its default per-method lifecycle), MSTest, and xUnit.net create a new test-class instance for every test method, so a constructor there runs before each test and does not carry state from one test to the next. NUnit, however, reuses one fixture instance for all tests in the class by default, as does JUnit 5 with
@TestInstance(Lifecycle.PER_CLASS): constructor-initialized mutable fields then bleed from one test into the next, and results depend on execution order. - No async setup. A constructor can't
await. Any setup that needs I/O, a DB connection, or a started server can't live there cleanly; people work around it with blocking calls or duplicated per-test code. - Readability/consistency. Reviewers and tooling expect fixtures in the conventional hook. Splitting initialization between a constructor and a setup method makes the real fixture hard to find and easy to get wrong.
Caveat — it's framework-dependent. This is not a universal rule. In xUnit.net the constructor is the idiomatic per-test setup (paired with IDisposable for teardown), and MSTest even ships an opposite, opt-in rule (MSTEST0020) preferring constructors over [TestInitialize]. Treat "Constructor Initialization" as a smell specifically where your framework provides a dedicated setup hook (JUnit, classic MSTest, NUnit) and you're sidestepping it.
##Treatment
Move field initialization out of the constructor and into the framework's setup hook, and prefer fresh per-test state over shared instances.
- Delete the test-class constructor; declare the fields and assign them in the setup hook (
@BeforeEach/setUp()for JUnit,[TestInitialize]for MSTest,beforeEachfor Jest/Vitest). - If setup is asynchronous, this is mandatory — the hook can
await; a constructor cannot. - For the related shared-fixture problem in JS/TS (see General Fixture / Test Order Dependency), build the fixture inside
beforeEach(re-assigning alet) rather than once atdescribe/module scope, so each test gets a clean object. - Keep teardown symmetric: pair
@BeforeEachwith@AfterEach(or, in xUnit.net's constructor style, implementIDisposable).
Before → after (JUnit):
// before
public class TagEncodingTest {
private final CryptoComponent crypto = new CryptoComponentImpl(...);
}
// after
public class TagEncodingTest {
private CryptoComponent crypto;
@BeforeEach
void setUp() {
crypto = new CryptoComponentImpl(...);
}
}
Related case (Jest/Vitest):
describe('Cart', () => {
let cart: Cart;
beforeEach(() => { cart = new Cart(); }); // fresh per test
it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
it('starts empty', () => { expect(cart.size).toBe(0); });
});
If the fixture is genuinely immutable and expensive (and your framework supports it), a one-time hook such as @BeforeAll/beforeAll is acceptable — but only for truly read-only shared state, never for objects tests mutate.
##Detected by
- tsDetect Constructor Initialization — Flags test classes that declare a constructor (the Constructor Initialization smell in the tsDetect catalog)
- mstest-analyzers MSTEST0019 — Prefer TestInitialize methods over constructors (opt-in MSTest analyzer)