Framework Design
A good framework lets testers write tests without knowing how the browser works. A bad framework buries business logic in XPath strings. Learn to design the former.
1 The Hook — Why This Matters
A large Auckland SaaS company built their automation suite over three years. Twelve engineers contributed. By year four, adding a single new test required modifying seven files across four directories. No one understood the full dependency graph. When the lead automation engineer left, the team spent six months reverse-engineering the framework before they could add a single new test.
Framework design is the difference between automation that scales and automation that fossilises. Good architecture lets junior engineers contribute on day one. Bad architecture traps knowledge in one person's head.
2 The Rule — The One-Sentence Version
Test scripts call business logic. Business logic calls core libraries. Test scripts never call core libraries directly.
This three-layer separation is the foundation of maintainable automation. Violate it, and your tests become tightly coupled to implementation details that change every sprint.
Senior engineer insight
The layer boundary between test scripts and business logic looks like unnecessary indirection until you're asked to migrate a 600-test suite from Selenium to Playwright on a six-week deadline. When every test routes through page objects and core adapters, you swap the driver in one file and the tests keep running. When tests call WebDriver directly, you're rewriting line by line. I've lived both scenarios — the well-layered suite took three days; the tightly-coupled one took three months and we still shipped broken tests.
The most common mistake: teams treat the business logic layer as optional boilerplate and write "just this one test" that calls the driver directly — then defend that pattern indefinitely because it's already in the codebase.
From the field
A Wellington government agency — one of the larger ones processing high-volume citizen transactions — had a bespoke Selenium framework that grew organically over four years. The team assumed the configuration system was solid because "we've never had environment issues." What they discovered during a DR drill was that environment URLs were hardcoded in 47 different test files, each using slightly different string formatting. Switching from UAT to staging meant grep-and-replace across the entire repository, then manual cleanup of the edge cases. After rebuilding the config layer with a single YAML-per-environment pattern and a typed Config class, the same environment switch became a one-line CLI flag. The lesson that generalises: configuration debt is invisible until the moment it becomes catastrophic.
3 The Analogy — Think Of It Like...
A restaurant kitchen.
The waiter (test script) takes orders and delivers food. The chef (business logic) prepares the dish using recipes and ingredients. The supplier (core library) delivers fresh produce. The waiter doesn't call the supplier directly — that would create chaos every time the supplier changes their delivery schedule. Each layer has one job and one interface to the layer above.
4 Watch Me Do It — Step by Step
Here is the three-layer framework model from ISTQB CTAL-TAE v2.0.
- Test Script LayerContains test cases, test data references, and suite orchestration. It is SUT-dependent.
# test_scripts/test_login.py def test_login_success(login_page): login_page.goto() login_page.login("user@example.com", "password123") assert login_page.is_logged_in() - Business Logic LayerContains page objects, API wrappers, and user flow facades. Also SUT-dependent.
# business_logic/pages/login_page.py class LoginPage: def __init__(self, page): self.page = page self.username = page.get_by_label("Username") self.password = page.get_by_label("Password") self.submit = page.get_by_role("button", name="Sign in") def login(self, user, pw): self.username.fill(user) self.password.fill(pw) self.submit.click() - Core Library LayerContains drivers, config loaders, reporting, and retry logic. SUT-independent and reusable across projects.
# core/config_loader.py import yaml from pathlib import Path class Config: def __init__(self, env: str): self.env = env self._data = yaml.safe_load( Path(f"config/environments/{env}.yml").read_text() ) @property def base_url(self) -> str: return self._data["base_url"]
| Criterion | Build Custom | Use Existing |
|---|---|---|
| Team size | >5 engineers with framework expertise | Any size |
| SUT complexity | Custom protocols, embedded systems | Standard web/mobile/API |
| Time to first test | Weeks to months | Hours to days |
| Maintenance budget | Dedicated TAE for upkeep | Community/vendor handles it |
5 When to Use It / When NOT to Use It
✅ Design a custom framework when...
- You have >5 automation engineers
- Multiple projects need shared core libraries
- The SUT has custom protocols or unusual requirements
- Off-the-shelf tools don't integrate with your stack
❌ Use existing tools when...
- Team is small (<3 engineers)
- Standard web/mobile/API testing
- Time to value matters more than customisation
- You don't have budget for framework maintenance
6 Common Mistakes — Don't Do This
🚫 The Record and Replay Trap
I used to think: Recording user actions and replaying them is fast automation.
Actually: Recorded scripts generate brittle locators, have no modularity, and break on every UI change. They are useful for prototyping but must be refactored into structured POM/framework scripts before entering the codebase.
🚫 Test scripts calling core libraries directly
I used to think: Bypassing the business logic layer saves a few lines of code.
Actually: It couples tests to driver implementation. When you switch from Selenium to Playwright, every test breaks instead of just the core adapter. Always route calls through the business logic layer.
🚫 Over-engineering early
I used to think: Starting with full dependency injection and microservices architecture shows professionalism.
Actually: Start lean. Validate that the team actually needs the abstraction before building it. A simple Page Object model with a config file beats a half-implemented DI container that no one understands.
7 Now You Try — Interview Warm-Up
Scenario: You join a team where every test file imports selenium.webdriver directly, creates its own driver instance, and uses raw XPath locators. The suite has 400 tests and takes 3 hours to run. The team wants to migrate to Playwright.
What is your migration strategy?
The strategy:
- Layer first: Extract all raw WebDriver calls into a core adapter module. Tests now call
core.driver.find()instead ofdriver.find_element(). - Build page objects: Create business logic layer page objects that use the core adapter. Migrate tests incrementally, file by file.
- Swap the adapter: Rewrite the core adapter to use Playwright instead of Selenium. Because tests call the adapter, not the driver directly, most tests continue working with minimal changes.
- Parallelise: Once on Playwright, shard tests across workers. Target: 3 hours to 20 minutes.
Why teams fail here
- Skipping the core library layer entirely: Teams put driver setup, wait logic, and screenshot capture directly in page objects, then wonder why adding a new page object requires copy-pasting 40 lines of boilerplate. Without a reusable core layer, every page object becomes a slightly different snowflake.
- Reporting as an afterthought: Test results get dumped to console and called done. In NZ government projects subject to OIA requests or audit (Revenue NZ, CoverNZ, Benefits NZ), you often need reproducible evidence of what ran, when, and against which environment — console output doesn't cut it. Design the reporting contract from day one, not sprint fifteen.
- One config file for all environments: A single
config.ymlwith environment variables commented in and out is not a configuration system. It's a minefield. Environment-specific configs stored separately — and loaded by name at runtime — eliminate the class of outages where someone runs prod data against the UAT database because the wrong block was uncommented. - No decision log for framework choices: Six months after a senior engineer joins, rewrites the retry mechanism, and leaves, no one remembers why the original approach existed. Document why you chose the patterns you chose. The decision log costs an hour; the archaeology of reverse-engineering undocumented framework choices costs weeks.
Key takeaway
A framework that took three days to design will save three months of migration pain — the layer boundaries you enforce on day one are the only thing standing between your team and a complete rewrite every time your tooling changes.
8 Self-Check — Can You Actually Do This?
Click each question to reveal the answer. If you got all three, you're ready to practice.
Q1. What are the three layers of a test automation framework?
Test Scripts (test cases), Business Logic (page objects, API wrappers), and Core Libraries (drivers, config, reporting). Test scripts call business logic; business logic calls core libraries.
Q2. Why should test scripts never call core libraries directly?
It creates tight coupling between tests and driver implementation. When the underlying tool changes (Selenium to Playwright), every test breaks instead of just the adapter in the core layer.
Q3. When is building a custom framework justified over using Playwright or Cypress?
When you have >5 engineers, multiple projects needing shared libraries, custom protocols, or deep integration requirements that off-the-shelf tools don't support. For most teams, existing tools are the right choice.