advanced21 min

Fixtures and Dependency Injection Concepts

How Playwright's fixture system is really a dependency-injection mechanism, why fixture composition avoids duplicated setup logic, and how to design fixtures that stay independently testable and understandable.

What you'll learn

  • Explain how a fixture is a form of dependency injection -- a test declares what it needs, without knowing how it's constructed
  • Design a fixture that depends on another fixture, avoiding duplicated setup logic across the framework
  • Explain the tradeoff between fixture depth (many small, composed fixtures) and fixture complexity (fewer, larger fixtures)

Prerequisites

Explanation

No real fixture executes against a real browser in this lesson's exercises -- they model fixture composition and dependency graphs as data, using genuine JavaScript/TypeScript execution.

A Playwright fixture is, conceptually, a form of dependency injection: a test function declares, as a parameter, WHAT it needs ({ loggedInPage }), without the test itself containing the logic for HOW that thing gets constructed (navigating to a login page, filling credentials, waiting for redirect). The fixture definition owns that construction logic in exactly one place, and every test that needs a logged-in page simply declares the dependency and receives an already-built one — this is the same core idea dependency injection provides in application code: callers declare what they depend on, and a separate mechanism is responsible for actually providing it.

Fixtures can depend on other fixtures, forming a composition chain — a loggedInPage fixture might itself depend on a lower-level testUser fixture (from Lesson 3) to know which credentials to log in with, which might itself depend on a config fixture for the base URL to navigate to. This composition is what avoids duplicating setup logic: without it, every test (or every fixture) that needs a logged-in page would need to re-implement the same login flow directly, and a change to the login flow would require updating every one of those copies instead of just the one fixture that owns it.

There's a genuine, honest tradeoff in how deep to make this composition: many small, single-purpose fixtures (a config fixture, a testUser fixture, a loggedInPage fixture built from both) keep each piece simple, independently understandable, and reusable in different combinations — but a very deep chain can make it harder to trace, for a given test, exactly what setup work is actually happening before it runs. Fewer, larger fixtures are more immediately readable in isolation but risk duplicating logic across them and being harder to reuse partially (a test that needs only the user, not the full logged-in page, can't easily get just that piece). There's no single universally correct depth — the goal is composing fixtures deliberately around genuine, reusable units of setup, not mechanically extracting everything into a fixture, or refusing to extract anything at all.

Example

Modeling a fixture dependency chain and detecting duplicated setup logic that composition would avoid, as data.

function resolveFixtureChain(fixtureName, fixtureGraph) {
  // Models resolving a fixture's full dependency chain, base-first.
  const deps = fixtureGraph[fixtureName] ?? [];
  const resolved = deps.flatMap((dep) => resolveFixtureChain(dep, fixtureGraph));
  return [...resolved, fixtureName];
}
const graph = { config: [], testUser: ["config"], loggedInPage: ["testUser"] };
console.log(resolveFixtureChain("loggedInPage", graph)); // ["config","testUser","loggedInPage"] -- built in dependency order

function wouldDuplicateSetupLogic(testsNeedingLoggedInState, hasSharedFixture) {
  if (hasSharedFixture) return false; // one fixture, reused by every test that needs it
  return testsNeedingLoggedInState > 1; // without composition, each test reimplements the same login flow
}
console.log(wouldDuplicateSetupLogic(5, false)); // true -- 5 separate, duplicated login implementations
console.log(wouldDuplicateSetupLogic(5, true));  // false -- one shared, composed fixture instead

Try it yourself

Call resolveFixtureChain with 'testUser' instead of 'loggedInPage', and confirm it resolves a shorter chain (config, then testUser).

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Guided exercise

Guided exercise

This models detecting a circular fixture dependency only -- no real fixture graph is used. Write hasCircularDependency(fixtureGraph, start), returning true if starting from start and following dependencies, you can reach start again. Use a visited Set and recursion.

Checks: detects a direct, two-fixture circular dependency · does not flag a valid, non-circular dependency chain · detects an indirect cycle spanning multiple fixtures

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Independent exercise

Independent exercise

This models choosing fixture depth based on genuine reuse only -- no real fixture is created. Write shouldExtractSeparateFixture(usedByMultipleTests, isIndependentlyMeaningful): return true only if BOTH are true.

Checks: extracts a fixture when both genuine reuse and independent meaning are present · does not extract for single-use setup · does not extract setup with no independent meaning

Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.

Loading editor…

Stuck? Get a hint.

Common mistakes

  • Reimplementing the same setup logic (like a login flow) directly inside multiple tests instead of extracting it into a shared, composable fixture -- a later change to that setup then requires updating every duplicated copy.
  • Building fixture dependency chains so deep that it becomes hard to trace what setup work actually happens before a given test runs -- composition should serve clarity, not obscure it.
  • Mechanically extracting every small piece of setup into its own fixture regardless of whether it's genuinely reused or independently meaningful -- this adds indirection without adding real value.

Knowledge check

Knowledge check

1. In what sense is a Playwright fixture a form of dependency injection?
2. Why does composing fixtures (one fixture depending on another) help avoid duplicated setup logic?
3. What's the honest tradeoff between many small, composed fixtures and fewer, larger ones?

Takeaway

Treat fixtures as dependency injection -- tests declare what they need, fixtures own how it's constructed. Compose fixtures from smaller, genuinely reusable pieces to avoid duplicated setup logic, but stay deliberate about depth -- extract a fixture when it's both reused and independently meaningful, not mechanically.

Summary

A fixture lets a test declare a dependency without containing the logic to construct it, mirroring dependency injection in application code. Composing fixtures from other fixtures (like loggedInPage depending on testUser) avoids duplicating setup logic across many tests. Fixture depth is a genuine, honest tradeoff between simplicity/reusability and traceability -- extraction should be deliberate, based on genuine reuse and independent meaning, not mechanical.

References

Your notes

Notes save automatically.