ConstructiCat Logo
CodeBust.
Browse section ▾

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/@AfterEach, base-class setup, and per-test re-initialization are silently bypassed or run in a confusing order relative to the constructor.
  • Asynchronous setup is impossible: you cannot await inside 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();
    }
}

JS/TS analog — fixture built once at describe/module scope (the constructor-equivalent) instead of per test:

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: final fields 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 (or beforeEach/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 and flakiness. In frameworks that reuse one instance (or when you stash the fixture in a shared/describe-scoped variable), mutations from one test bleed into the next. Tests pass or fail depending on execution order — false confidence and intermittent failures.
  • 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.

  1. Delete the test-class constructor; declare the fields and assign them in the setup hook (@BeforeEach/setUp() for JUnit, [TestInitialize] for MSTest, beforeEach for Jest/Vitest).
  2. If setup is asynchronous, this is mandatory — the hook can await; a constructor cannot.
  3. In JS/TS, build the fixture inside beforeEach (re-assigning a let) rather than once at describe/module scope, so each test gets a clean object.
  4. Keep teardown symmetric: pair @BeforeEach with @AfterEach (or, in xUnit.net's constructor style, implement IDisposable).

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

Before → after (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