ConstructiCat Logo
CodeBust.
Browse section ▾

Fixture générale.

Une configuration partagée construit une grande fixture « fourre-tout » couvrant les besoins de tous les tests, mais chaque test individuel n'en exploite qu'une petite partie.

##Signs and Symptoms

Un unique beforeEach/setUp (ou une configuration partagée au niveau du module) construit de nombreux objets, insère de nombreux enregistrements et met en place plusieurs mocks — et un test donné n'en touche qu'un ou deux. L'heuristique de détection issue du catalogue des odeurs de test est simple : tous les champs créés dans la configuration ne sont pas utilisés par toutes les méthodes de test.

Comment la reconnaître :

  • Le bloc de configuration est long et grossit à chaque ajout d'un nouveau test.
  • Pour comprendre un seul test, vous devez remonter et reconstituer par rétro-ingénierie quelles parties de la fixture lui importent réellement.
  • De nombreux tests échouent ou doivent être modifiés lorsque vous changez un objet partagé dont la plupart d'entre eux ne se soucient même pas.
  • La même fixture sert, depuis un seul endroit, des scénarios radicalement différents (création, validation, paiement, permissions).
// Odeur : une seule fixture pour tout ; chaque test n'en utilise qu'une infime partie
let user, admin, product, cart, order, paymentGateway, shippingZone;

beforeEach(() => {
  user            = createUser({ name: 'Ada' });
  admin           = createAdmin();
  product         = createProduct({ price: 10 });
  cart            = createCart(user, [product]);
  order           = createOrder(cart);
  paymentGateway  = mockGateway();
  shippingZone    = createZone('EU');
});

test('cart subtotal sums its line items', () => {
  // n'a besoin que de cart + product, mais paie quand même pour admin, order, gateway, zone…
  expect(cart.subtotal()).toBe(10);
});

##Reasons for the Problem

Pourquoi cela arrive

  • DRY appliqué de manière trop agressive. La configuration est regroupée dans un unique hook partagé pour « éviter la duplication », si bien que les besoins de chaque nouveau test viennent se greffer sur la même fixture.
  • Croissance organique. La fixture accumule des objets au fil de l'ajout des tests, et personne ne supprime ce que les anciens tests n'utilisent plus.
  • Inertie de la configuration implicite. Une fois qu'un gros beforeEach existe, ajouter une ligne de plus est plus facile que de créer une fixture ciblée pour le nouveau cas.

Pourquoi c'est nuisible

  • Lisibilité / tests en tant que documentation. Un test devrait se lire comme une relation claire de cause à effet. Lorsque l'essentiel de la fixture n'est que du bruit hors de propos, le lecteur ne peut pas distinguer ce qui détermine réellement le résultat. L'antidote de Meszaros, la Minimal Fixture (fixture minimale), existe précisément parce qu'un test utilisant la plus petite fixture possible est toujours plus facile à comprendre.
  • Maintenabilité. Une modification exigée par un seul test (par exemple ajouter un nouveau champ obligatoire à product) force à modifier une configuration partagée par tous les tests, ce qui provoque des ruptures en cascade et couple entre eux des tests sans rapport.
  • Fiabilité. Un état de fixture partagé et mutable laisse les tests s'influencer mutuellement et rend les échecs difficiles à localiser — une source classique d'instabilité dépendant de l'ordre d'exécution.
  • Rapidité. Chaque test paie le coût intégral de la construction de toute la fixture. La fixture générale figure parmi les causes des Slow Tests (tests lents).
  • Fausse confiance. Les tests deviennent surspécifiés vis-à-vis de détails accessoires de la fixture, de sorte qu'ils réussissent ou échouent pour des raisons sans rapport avec le comportement testé.

##Treatment

Visez une Minimal Fixture (fixture minimale) : chaque test ne met en place que ce dont il a besoin, et rien de plus.

  1. Inventoriez l'utilisation. Pour chaque champ partagé, dressez la liste des tests qui le lisent réellement. Tout ce qui n'est utilisé que par un sous-ensemble est candidat à être sorti de la configuration partagée.
  2. Déplacez la construction vers des Creation Methods / des constructeurs de données de test (Delegated Setup). Gardez des tests concis sans imposer une fixture géante — chaque test appelle un builder et ne surcharge que les attributs qui le concernent.
  3. Préférez une Fresh Fixture par test plutôt qu'une fixture partagée à longue durée de vie, ce qui élimine le couplage entre tests et l'état partagé mutable.
  4. Découpez par scénario. Si des groupes de tests partagent réellement une petite fixture, séparez-les dans des blocs describe ciblés (ou des classes de test), chacun avec sa propre configuration minimale.
  5. Ne gardez dans beforeEach que ce qui est vraiment commun et réduit ; déplacez le reste en ligne afin que la cause et l'effet se trouvent juste à côté de l'assertion.
// Avant : fixture générale dans un hook partagé (voir Signes & symptômes)

// Après : configuration minimale et déléguée — chaque test ne construit que ce dont il a besoin
test('cart subtotal sums its line items', () => {
  const cart = aCart().withItem(aProduct().price(10)).build();
  expect(cart.subtotal()).toBe(10);
});

test('checkout charges the payment gateway', () => {
  const gateway = mockGateway();
  const cart    = aCart().withItem(aProduct().price(10)).build();

  checkout(cart, gateway);

  expect(gateway.charge).toHaveBeenCalledWith(10);
});

Chaque test se lit désormais de haut en bas comme une histoire autonome, les builders absorbent le code répétitif, et modifier un scénario ne perturbe plus les autres.