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 insteadTry 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.
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.
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.
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
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.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.