Framework Goals & Core Principles
What the Framework Must Achieve
A test automation framework is not just a test runner with helpers. It is a product — with users (engineers), maintenance cost, and quality requirements.
| Goal | Description |
|---|---|
| Scalability | Supports 1000+ tests without structural degradation |
| Maintainability | Changes to the app don't ripple into hundreds of tests |
| Reusability | Shared logic lives once; tests call it, don't copy it |
| Stability | Low flakiness — failures mean bugs, not timing noise |
| Fast execution | Parallelised; CI feedback under 10 minutes |
| Easy onboarding | A new engineer runs and writes tests on day one |
Why These Goals Matter
A framework without scalability goals turns into a maintenance burden at 200+ tests. A framework without stability goals loses trust — engineers start ignoring failures. A framework without onboarding goals becomes a black box owned by one person.
All six goals interact. Optimise for all six from the start.
Core Design Principles
DRY — Don't Repeat Yourself
If the same HTTP call setup appears in three tests, it belongs in a shared client. If the same user creation logic appears twice, it belongs in a builder.
Repetition in tests means: one API change breaks 20 test files instead of one.
KISS — Keep It Simple
No abstract base class wrapping a wrapper wrapping a driver wrapping a client. One indirection level solves 90% of problems. Two solves 99%. Stop there.
Complexity that cannot be explained in two sentences does not belong in a framework.
SOLID in Test Code
| Principle | Application |
|---|---|
| Single Responsibility | Each page object / API client handles one domain |
| Open/Closed | Add new test types without modifying core framework |
| Liskov Substitution | Interchangeable drivers (mock vs real HTTP client) |
| Interface Segregation | Tests depend on narrow interfaces, not fat base classes |
| Dependency Inversion | Tests inject clients; don't construct them internally |
Separation of Concerns
Test file → orchestration only (arrange → act → assert)
Page objects → UI interactions and locators
API clients → HTTP communication
Builders → test data construction
Assertions → verification logic
Each layer has one job. Tests that mix all of these are impossible to maintain.
Test Independence
Every test must be able to run in isolation. No shared mutable state between tests. No test that only passes after another test has run.
# Bad — depends on test execution order
def test_update_user():
# assumes user was created by test_create_user
...
# Good — creates its own data
def test_update_user(user_factory):
user = user_factory.create(name="Alice")
...
Deterministic Behaviour
Same code, same input → same result every time, on any machine, in any order. Non-determinism is the root cause of flakiness.
Sources of non-determinism to eliminate:
- Real timestamps → use frozen time
- Random values without seed → use builders with fixed defaults
- Race conditions → use explicit waits, not sleep()
- Shared DB state → clean up after each test or use unique identifiers
Consequences of Ignoring Principles
| Ignored Principle | Symptom |
|---|---|
| DRY | One API change breaks 30 test files |
| KISS | Only the author can debug failures |
| Test independence | Tests pass alone, fail in suite |
| Determinism | Flaky tests that developers learn to ignore |
| Separation of concerns | Tests become integration scripts, not test cases |