ConstructiCat Logo
CodeBust.
Browse section ▾

Initialisation dans le constructeur.

Une classe de test initialise les champs de sa fixture dans un constructeur au lieu du hook de mise en place dédié du framework (setUp / @BeforeEach / TestInitialize), contournant le cycle de vie des tests.

##Signs and Symptoms

Une classe de test possède un constructeur explicite qui construit le système sous test, les mocks ou les champs partagés — un travail qui revient au hook de mise en place du framework (setUp() / @Before / @BeforeEach dans JUnit, [TestInitialize] dans MSTest, beforeEach dans Jest/Vitest).

Signes révélateurs :

  • La classe de test déclare un constructeur qui affecte des champs final/d’instance.
  • La mise en place de la fixture vit en dehors du cycle de vie réellement géré par le framework — de sorte que @BeforeEach/@AfterEach, la mise en place de la classe de base et la réinitialisation par test sont silencieusement contournés ou exécutés dans un ordre déroutant par rapport au constructeur.
  • La mise en place asynchrone est impossible : vous ne pouvez pas faire await dans un constructeur, si bien que le travail de fixture asynchrone est forcé au mauvais endroit ou dupliqué.

Forme JUnit canonique (tirée du catalogue des odeurs de test) :

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

    public TagEncodingTest() {                 // odeur : initialisation dans le constructeur
        crypto = new CryptoComponentImpl(new TestSecureRandomProvider());
        tagKey = TestUtils.getSecretKey();
    }
}

Équivalent JS/TS — fixture construite une seule fois à la portée describe/module (l’équivalent du constructeur) au lieu de par test :

describe('Cart', () => {
  const cart = new Cart();          // construite une seule fois, fait fuir l'état entre les tests
  it('adds an item', () => { cart.add(apple); expect(cart.size).toBe(1); });
  it('starts empty', () => { expect(cart.size).toBe(0); }); // échoue : voit apple
});

##Reasons for the Problem

Pourquoi cela arrive

  • Les développeurs qui ne connaissent pas le rôle du hook de mise en place se tournent vers la fonctionnalité du langage qu’ils maîtrisent déjà — le constructeur — pour initialiser les champs.
  • Cela paraît plus propre : des champs final assortis d’un constructeur semblent idiomatiques, et un IDE peut même générer le constructeur à votre place.
  • Du cargo-culting depuis les classes de production, où l’initialisation dans le constructeur est le bon modèle.

Pourquoi c’est nuisible

  • Contourne le cycle de vie des tests. Les frameworks promettent un état neuf pour chaque test via le hook de mise en place et une paire @BeforeEach/@AfterEach (ou beforeEach/afterEach). La logique cachée dans un constructeur se situe en dehors de ce contrat, si bien que le nettoyage, la mise en place de la classe de base et les garanties d’ordonnancement peuvent ne pas s’appliquer comme prévu.
  • Fuite d’état et instabilité. Dans les frameworks qui réutilisent une seule instance (ou lorsque vous stockez la fixture dans une variable partagée/de portée describe), les mutations d’un test débordent sur le suivant. Les tests réussissent ou échouent selon l’ordre d’exécution — fausse confiance et échecs intermittents.
  • Pas de mise en place asynchrone. Un constructeur ne peut pas faire await. Toute mise en place nécessitant des E/S, une connexion à une base de données ou un serveur démarré ne peut pas y résider proprement ; on contourne le problème avec des appels bloquants ou du code dupliqué par test.
  • Lisibilité/cohérence. Les relecteurs et l’outillage s’attendent à trouver les fixtures dans le hook conventionnel. Répartir l’initialisation entre un constructeur et une méthode de mise en place rend la vraie fixture difficile à trouver et facile à mal configurer.

Mise en garde — cela dépend du framework. Ce n’est pas une règle universelle. Dans xUnit.net, le constructeur est la mise en place idiomatique par test (associé à IDisposable pour le nettoyage), et MSTest propose même une règle inverse et optionnelle (MSTEST0020) qui privilégie les constructeurs plutôt que [TestInitialize]. Considérez « Initialisation dans le constructeur » comme une odeur spécifiquement là où votre framework fournit un hook de mise en place dédié (JUnit, MSTest classique, NUnit) et que vous le contournez.

##Treatment

Déplacez l’initialisation des champs hors du constructeur, vers le hook de mise en place du framework, et préférez un état neuf par test plutôt que des instances partagées.

  1. Supprimez le constructeur de la classe de test ; déclarez les champs et affectez-les dans le hook de mise en place (@BeforeEach/setUp() pour JUnit, [TestInitialize] pour MSTest, beforeEach pour Jest/Vitest).
  2. Si la mise en place est asynchrone, c’est obligatoire — le hook peut faire await ; un constructeur, non.
  3. En JS/TS, construisez la fixture à l’intérieur de beforeEach (en réaffectant un let) plutôt qu’une seule fois à la portée describe/module, afin que chaque test obtienne un objet propre.
  4. Gardez un nettoyage symétrique : associez @BeforeEach à @AfterEach (ou, dans le style à constructeur de xUnit.net, implémentez IDisposable).

Avant → après (JUnit) :

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

// après
public class TagEncodingTest {
    private CryptoComponent crypto;

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

Avant → après (Jest/Vitest) :

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

Si la fixture est véritablement immuable et coûteuse (et que votre framework le permet), un hook unique tel que @BeforeAll/beforeAll est acceptable — mais uniquement pour un état partagé véritablement en lecture seule, jamais pour des objets que les tests modifient.

##Detected by