level 14 / framework-design-questions
Framework Design Questions
Master the most common framework architecture interview questions — POM, fixtures, helpers, and test structure decisions.
What Interviewers Are Testing
Framework design questions assess whether you can build a maintainable, scalable test suite — not just write individual tests. They want to know:
- Do you separate concerns (page logic vs. test logic)?
- Do you make tests readable to non-engineers?
- Do you reduce duplication without over-abstracting?
- Do you handle cross-cutting concerns (auth, data, reporting) cleanly?
Common Questions & Strong Answers
”How would you structure a Playwright test framework from scratch?”
Strong answer structure:
- Start with project goals (team size, app type, CI requirements)
- Define layers (page objects, fixtures, utilities, tests)
- Explain config strategy (environments, reporters)
- Describe CI integration
playwright-tests/
├── e2e/
│ ├── auth/ # auth flow tests
│ ├── checkout/ # checkout flow tests
│ └── smoke/ # cross-cutting smoke suite
├── pages/ # Page Object classes
├── fixtures/ # Custom Playwright fixtures
├── utils/ # Pure helpers (data factories, API clients)
├── playwright.config.ts
└── global-setup.ts # storageState, DB seed
”What is the Page Object Model and when would you NOT use it?”
POM:
// pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
await this.page.getByLabel('Email').fill(email);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign in' }).click();
}
async getErrorMessage() {
return this.page.getByRole('alert').textContent();
}
}
When NOT to use POM:
- One-off scripts or exploratory tests
- Very simple apps with 2-3 pages
- When the overhead of maintaining page classes exceeds the benefit
- Teams that prefer a more functional/procedural style (use plain helpers instead)
“How do you share state between tests without coupling them?"
// ❌ Shared mutable state — order-dependent, fragile
let userId: string;
test.beforeAll(async () => { userId = await createUser(); });
test('uses userId', async ({ page }) => { /* depends on above */ });
// ✅ Fixture-based isolation — each test self-contained
const test = base.extend<{ user: User }>({
user: async ({ request }, use) => {
const user = await createUserViaApi(request);
await use(user);
await deleteUserViaApi(request, user.id); // teardown
},
});
"How do you handle authentication across your test suite?”
// global-setup.ts: authenticate once, save storageState
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER!);
await page.getByLabel('Password').fill(process.env.TEST_PASS!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.context().storageState({ path: 'playwright/.auth/user.json' });
await browser.close();
}
// playwright.config.ts
export default defineConfig({
use: { storageState: 'playwright/.auth/user.json' },
globalSetup: './global-setup.ts',
});
Questions About Fixtures
”What’s the difference between a fixture and a helper function?”
| Fixture | Helper function | |
|---|---|---|
| Lifecycle | Setup + teardown, scoped to test | No lifecycle management |
| DI | Injected by Playwright automatically | Called explicitly |
| Scope | test / worker / file | N/A |
| Use for | Resources with cleanup (DB, auth, browser context) | Pure transformations (format date, generate ID) |
“When would you use worker scope vs test scope for a fixture?”
testscope: State that must be isolated per test (user session, created records)workerscope: Expensive shared resources safe to reuse (DB connection pool, authenticated context for read-only tests)