SOLID tells you what each unit of code should do. DRY, KISS, and YAGNI tell you how much code to write and how complex to make it. Automation engineers routinely fail in two opposite directions: the chaotic framework where everything is duplicated and nothing is shared, and the over-engineered framework where a generic plugin system with reflection-based field mapping and a configurable adapter layer was built before a single real test ran. Both fail for the same reason — they didn't apply these three principles. DRY prevents the first failure. KISS and YAGNI prevent the second.
DRY — Don't Repeat Yourself
DRY's precise definition: every piece of knowledge in a system should have exactly one authoritative representation. When that piece of knowledge changes, it should change in one place.
In test code, the most costly violations of DRY are:
Selector duplication. The login button selector appearing in 12 test files. When it changes, it changes in 12 places — and you discover 3 of those places on the day a release is blocked.
// Not DRY — selector in the test
driver.findElement(By.cssSelector("[data-testid='submit']")).click();
// ...repeated in 12 tests
// DRY — selector in the page object, once
public class LoginPage {
private final By submitButton = By.cssSelector("[data-testid='submit']");
public void clickSubmit() { find(submitButton).click(); }
}Setup duplication. The same 8-line login block at the top of 20 tests. If the login flow adds a two-factor step, you update 20 methods.
# Not DRY — setup duplicated in every test
def test_checkout(page):
page.goto("/login")
page.fill("#email", "alice@test.com")
page.fill("#password", "s3cr3t")
page.click("#submit")
page.wait_for_url("**/dashboard")
# ...actual test
# DRY — login extracted to a fixture, called once
@pytest.fixture
def authenticated_page(page, login_page):
login_page.login("alice@test.com", "s3cr3t")
return page
def test_checkout(authenticated_page):
# actual test starts here — login is a fixtureConfig duplication. The base URL appearing in 8 places. The timeout value appearing in 15 places. Change either, and you're doing a global search.
DRY has limits. The rule is about knowledge duplication, not textual similarity. Two tests that each do a 3-line setup for different reasons are not violating DRY — they coincidentally look similar. The test for "unauthenticated access" and the test for "login form validation" both navigate to /login, but they're testing different things. Extracting them into a shared helper to eliminate 2 lines of similarity is premature abstraction, not DRY.
KISS — Keep It Simple, Stupid
KISS: the simplest solution that correctly solves the problem is the right solution. Complexity has a cost. Every layer of abstraction, every design pattern, every configuration option makes the framework harder to understand, harder to debug, and harder to onboard new engineers into.
The KISS test: can a new engineer understand this component in 5 minutes without asking questions?
// Over-complex — reflection-based field mapping for a 2-environment use case
public class ConfigProvider {
@Inject @Named("config.source")
private ConfigSourceFactory factory;
public <T> T get(Class<T> type) {
return ReflectionUtils.mapFields(factory.resolve(), type);
}
}
// KISS — reads properties from one of two sources, returns strings
public class Config {
private static final String ENV = System.getenv().getOrDefault("ENV", "staging");
public static String baseUrl() {
return ENV.equals("prod")
? "https://myapp.com"
: "https://staging.myapp.com";
}
}Both solve "switch between environments." The first requires understanding dependency injection, factory patterns, reflection, and generic types — for a problem that only has two environments. The second requires understanding a conditional. When the staging URL changes, which one does a junior engineer fix without help?
KISS doesn't mean writing bad code. It means not writing more code than the problem requires. A clean page object with private locator fields and public action methods is simple and good. A 15-method abstract generic page object base with type parameters and method chaining fluent API for a 30-test suite is complex and unnecessary.
YAGNI — You Aren't Gonna Need It
YAGNI: don't build it until you need it. Not "might need it," not "will probably need it in Q3" — until the need is concrete and present.
The most common YAGNI violations in test frameworks:
- A "plugin system" for future data sources that only CSV is ever used with.
- A "reporter registry" where reporters can register themselves — when Allure is the only reporter and will be forever.
- A generic
AbstractPageFactory<T extends BasePage>when all pages are created withnew PageName(driver). - Configuration options for features that don't exist in the application yet.
Each of these was built "for when we need it." The cost is paid immediately in complexity, maintenance, and onboarding time. The benefit is deferred until the future requirement arrives — and often doesn't.
// YAGNI violation — plugin system for something that uses only JSON files
interface DataSourcePlugin {
read(path: string): TestData[];
getFormat(): string;
supportsStreaming(): boolean;
}
// YAGNI-compliant — just read the JSON file
function loadUsers(path: string): User[] {
return JSON.parse(fs.readFileSync(path, "utf-8"));
}When you need to support CSV in addition to JSON, write the CSV reader. When you need streaming, add streaming. Don't build for a problem you don't have.
Over-engineered vs right-sized framework
Over-engineered
Generic plugin system for 1 data source
6 abstraction layers for 30 tests
Type-parameterised page factory
Reflection-based config mapping
New engineer needs 2 weeks to add a test
Most code is infrastructure, not tests
Right-sized
Simple data reader for the format in use
Layered but not nested abstraction
new LoginPage(driver) — that's it
Config reads properties file + env vars
New engineer writes first test in 30 minutes
Most code is actual tests
The balance — DRY vs KISS vs YAGNI
The three principles sometimes pull in different directions. DRY says "extract the shared logic." KISS says "don't create more abstraction than necessary." YAGNI says "don't generalise until you have the second use case."
The practical synthesis:
- Duplicate once, extract the second time. The first occurrence of something stays inline. The second occurrence triggers extraction. The third occurrence confirms the extraction was right. This is the "rule of three" — a useful YAGNI-compatible DRY heuristic.
- Extract function, not pattern. DRY an operation by extracting it to a method. Only reach for a design pattern (factory, registry, plugin) when you have a concrete extensibility need.
- Complexity must earn its keep. Every abstraction that a new engineer can't understand in 5 minutes has a cognitive cost. That cost must be repaid by a proportional reduction in duplication or maintenance effort.
⚠️ Common mistakes
- Over-applying DRY to test scenarios. Two tests that look similar often have different intent — a happy-path test and an error-path test both log in, but they're testing different things. Forcing them to share a base scenario obscures what each test actually verifies. Tests should be readable independently, not DRY-optimised.
- YAGNI as a justification for poor structure. "We don't need page objects yet — YAGNI" is wrong. Page objects address a problem you have the moment you have 2+ tests that interact with the same page. YAGNI prevents building plugin systems; it doesn't prevent building obvious necessities.
- KISS as a justification for duplicated code. "This abstraction is too complex — KISS" is sometimes correct (plugin system for 1 data source). It's wrong when the "simpler" alternative is copy-pasting 10 lines into 20 tests. Duplicated code is also complex — just in a hidden, distributed way.
🎯 Practice task
Complexity and duplication audit — 25 minutes.
- DRY scan. Run a search for your most common selector (likely the login button or a submit button) across your test project. Count the occurrences. Each one beyond the first is a DRY violation. Track how many files would break if that selector changed today.
- KISS audit. Open the most complex class in your framework. Not the biggest — the most conceptually complex. Read it cold, as if you've never seen it before. If you can't understand its purpose in 5 minutes without comments, it fails KISS. Identify the source of the complexity: is it solving a real problem, or a hypothetical one?
- YAGNI inventory. List every abstraction in your framework that has exactly one concrete implementation. A
BrowserFactorywith only one browser class. AReporterRegistrywith one reporter. ADataSourceAdapterwith one data source. These are YAGNI candidates — they could be simplified to the concrete implementation until a second is needed. - Fix one. Choose the highest-impact issue from steps 1–3 and fix it. DRY: extract to a page object or fixture. KISS: inline the abstraction if it has no value. YAGNI: replace the abstraction with the concrete implementation.
- Stretch — the "explain in 5 minutes" test. Ask a junior engineer or a colleague unfamiliar with your framework to read your most complex class. Time how long it takes them to explain what it does. If it takes more than 5 minutes, the class fails KISS. Their confusion is a design signal, not a capability gap.
This lesson completes Chapter 2. Chapter 3 moves from principles to patterns: Page Object Model, Singleton, Factory, and Builder — the four design patterns every production test framework uses and why each one earns its place.