World’s largest virtual agentic engineering & quality conference
Testing patterns explained: 11 test design patterns with code examples covering unit testing, automation design patterns, and anti-patterns to avoid.

Bhavya Hada
Author

Salman Khan
Reviewer
Last Updated on: August 7, 2026
A developer renames a CSS class. Two hundred tests go red. None of them found a bug, and a team spends the afternoon updating selectors that were copy-pasted across two hundred files. The suite did not catch a regression, it manufactured one.
Testing patterns exist to stop that. They are reusable answers to problems that recur in every suite: where setup lives, how test data is built, how the UI is addressed, and what shape the whole portfolio should take.
TL;DR
Testing patterns are reusable solutions to problems that recur when writing and maintaining tests. They sit at three levels: unit testing patterns that shape a single test, test automation design patterns that shape how tests address the application, and strategy patterns that shape the whole suite.
Which testing pattern should you adopt first?
Start with Arrange-Act-Assert for structure and Page Object Model for UI addressing, because together they remove the two biggest sources of duplication. Add Test Data Builder when fixture setup starts repeating, and only reach for Screenplay when page objects have already grown unwieldy.
Testing patterns are named, reusable solutions to problems that recur in every test suite: duplicated setup, fragile UI addressing, unmanageable test data, and a suite whose shape makes it slow and untrustworthy. They operate at three levels, from the structure of a single test up to the balance of the entire portfolio.
The canonical reference is Gerard Meszaros' xUnit Test Patterns, whose companion site catalogues the patterns most test frameworks now assume. The three levels are:
Most articles mix all three, which is why teams adopt Page Object Model and then wonder why their suite is still slow. Page objects fix duplication, not suite shape. Those are different problems with different patterns, and this guide keeps them in separate sections for that reason.
Because skipping them produces tests that fail for reasons unrelated to the code. Shared mutable state, ad hoc setup, and duplicated selectors are three of the biggest causes of a flaky test, and each has a named pattern that removes it.
Google measured the scale of that noise on its own corpus and reported in a 2016 Google Testing Blog engineering post that it saw a continual rate of about 1.5% of all test runs reporting a flaky result, defining a flaky result as a test that both passes and fails against the same code. That figure remains the most-cited industry baseline.
The second reason is readability. A test is documentation that executes, and Meszaros makes exactly this argument for structuring tests into visible phases, noting that clearly identifying the four phases makes the intent of the test much easier to see.
Five patterns shape an individual test. Four-Phase Test and Arrange-Act-Assert structure the test body, Given-When-Then expresses it in business language, and Test Data Builder and Object Mother supply the data it needs. Each entry below ends with the point at which the pattern stops paying off.
The structural pattern everything else builds on. The xUnit Test Patterns catalogue answers the question "how do we structure our test logic to make what we are testing obvious?" with a single instruction: structure each test with four distinct parts executed in sequence.
Those parts are fixture setup, exercise SUT, result verification, and fixture teardown. The fourth phase puts the world back into the state you found it, which is what stops one test leaking into the next.
test('cart total includes tax', async () => {
// 1. fixture setup
const cart = new Cart({ taxRate: 0.2 });
cart.add({ sku: 'BOOK-1', price: 100 });
// 2. exercise SUT
const total = cart.total();
// 3. result verification
expect(total).toBe(120);
// 4. fixture teardown
cart.clear();
});When it stops paying off: modern runners handle teardown through fixtures and hooks, so an explicit fourth phase in every test becomes noise. Move teardown into a fixture as soon as more than two tests repeat it.
AAA is Four-Phase Test with teardown delegated to the framework. Arrange the state, act on the system, assert the outcome. It is the most widely used structural pattern in unit testing because it maps onto how people describe behavior out loud.
test('applies free shipping above threshold', () => {
// Arrange
const order = anOrder().withSubtotal(75).build();
// Act
const shipping = calculateShipping(order);
// Assert
expect(shipping).toBe(0);
});One rule keeps it useful: a single act per test. When a test has two act steps it is really two tests, and a failure no longer tells you which behavior broke.
When it stops paying off: never, for unit tests. For long end-to-end journeys the three-block shape becomes artificial, and Given-When-Then or Screenplay reads better.
The same three phases expressed in business language so non-engineers can read and review the specification. It is the structural backbone of behavior-driven development and the reason Cucumber-style tools exist.
Feature: Checkout shipping
Scenario: Free shipping above the threshold
Given a customer has 75 dollars of items in the cart
When they proceed to checkout
Then shipping is freeGWT is the structural core of behavior-driven development, and Cucumber testing is the most common way teams execute it.
When it stops paying off: when nobody outside the engineering team reads the feature files. At that point the natural-language layer is pure overhead, and plain AAA tests are cheaper to maintain.
A fluent chain that produces a valid object by default and lets each test override only the field under test. It solves the problem where adding a required constructor argument breaks every test that builds that object.
class CustomerBuilder {
constructor() {
this.data = { name: 'Test User', age: 30, country: 'US', verified: true };
}
withAge(age) { this.data.age = age; return this; }
unverified() { this.data.verified = false; return this; }
build() { return new Customer(this.data); }
}
const aCustomer = () => new CustomerBuilder();
// the test states only what matters to it
const minor = aCustomer().withAge(16).build();The readability win is that withAge(16) announces the one fact the test depends on. Everything else is deliberately uninteresting. Where the data must outlive the test run, pair this with a test data management approach.
When it stops paying off: when builders acquire conditional logic. A builder with branching is a second implementation of your domain model, and it will drift from the real one.
Named factory methods that return canonical, ready-made fixtures. Where a builder composes, an Object Mother hands you a known character from the domain.
const Customers = {
adult: () => new Customer({ name: 'Ada', age: 34, verified: true }),
minor: () => new Customer({ name: 'Sam', age: 16, verified: true }),
unverified: () => new Customer({ name: 'Lee', age: 41, verified: false }),
};
const customer = Customers.minor();When it stops paying off: at combinatorial growth. The moment you need an unverified minor from Germany you are writing a method per combination, which is precisely the problem Test Data Builder solves. Many suites use both: mothers for the three or four archetypes, builders for variations. The unit testing guide covers where these fixtures fit in the wider practice.
Note: Patterns reduce maintenance, but they cannot tell you which browser broke a test. TestMu AI captures video, network logs, and console output on every session so a failure is diagnosable without a rerun. Try TestMu AI free!
Four patterns govern how a test drives the application. Page Object Model and Screenplay model the interface, Factory centralises driver creation, and Fluent Interface chains the calls. Together they decide whether a markup change costs you one edit or two hundred.
The single highest-value pattern in UI automation, and the fix for the scenario that opened this article. One class per page or component owns the locators and exposes intent-revealing methods; tests call those methods and never touch a selector.
class LoginPage {
constructor(page) {
this.page = page;
this.username = page.getByLabel('Username');
this.password = page.getByLabel('Password');
this.submit = page.getByRole('button', { name: 'Log in' });
}
async loginAs(user, pass) {
await this.username.fill(user);
await this.password.fill(pass);
await this.submit.click();
}
}
// the test never sees a selector
await new LoginPage(page).loginAs('standard_user', 'secret_sauce');A rename now edits one file. The deeper treatment, including inheritance and component objects, is in the Page Object Model guide, with framework-specific walkthroughs in Playwright Page Object Model and Cypress Page Object Model.
When it stops paying off: when page objects turn into 500-line classes carrying locators, workflow logic, and assertions at once. Assertions in particular belong in the test, not the page object, because a page object that asserts cannot be reused by a test expecting failure.
Screenplay replaces pages with actors who perform tasks using abilities, and ask questions about state. It applies single-responsibility to what POM lumps together, so behavior composes instead of being inherited.
const james = actorCalled('James').whoCan(BrowseTheWeb.using(page));
await james.attemptsTo(
LogIn.withCredentials('standard_user', 'secret_sauce'),
AddToCart.theItem('Backpack'),
Checkout.now()
);
await james.asks(CartTotal.displayed()).shouldEqual('$29.99');Tasks are reusable across actors, which is what makes multi-role scenarios (an admin and a customer in one test) readable.
When it stops paying off: on small suites. Screenplay's indirection costs more than it saves below roughly a few dozen tests, so adopt it as a response to POM pain rather than as a starting point.
A classic object-oriented pattern that earns its place in test frameworks by centralising driver and browser creation, so switching from local to cloud execution is a config change rather than an edit across every test.
class BrowserFactory {
static async create(target = process.env.TARGET) {
if (target === 'cloud') {
const caps = {
browserName: 'Chrome',
browserVersion: 'latest',
'LT:Options': {
platform: 'Windows 11',
build: 'Design Patterns Demo',
user: process.env.LT_USERNAME,
accessKey: process.env.LT_ACCESS_KEY,
},
};
return chromium.connect({
wsEndpoint:
'wss://cdp.lambdatest.com/playwright?capabilities=' +
encodeURIComponent(JSON.stringify(caps)),
});
}
return chromium.launch();
}
}When it stops paying off: when the factory becomes a chain of conditionals covering every browser and environment. Split it per concern before it turns into the framework's most-edited file.
Method chaining where each call returns the next meaningful object, so a workflow reads as one sentence. It is what makes builders pleasant and what lets page objects express navigation.
// each method returns the page you land on
await new LoginPage(page)
.loginAs('standard_user', 'secret_sauce') // returns InventoryPage
.addToCart('Backpack') // returns InventoryPage
.openCart() // returns CartPage
.checkout(); // returns CheckoutPageWhen it stops paying off: when chains hide branching. A fluent chain that needs an if statement in the middle should be plain sequential calls, because a broken chain produces a stack trace that points at the wrong link.
Two patterns govern the whole portfolio rather than any single test. The test pyramid sets the ratio between fast and slow tests, and contract testing replaces cross-service end-to-end tests with per-consumer agreements. No amount of well-structured individual tests fixes a suite of the wrong shape, which is where a test strategy starts.
Martin Fowler describes the test pyramid as a way of thinking about how different kinds of automated tests should be used to create a balanced portfolio, and states the core rule plainly: you should have many more low-level unit tests than high-level tests running through a GUI.
His reasoning is the argument for every other pattern here. Tests that run end-to-end through the UI are, in his words, brittle, expensive to write, and time consuming to run. Fowler credits Mike Cohn's Succeeding with Agile (2009) for popularising the model.
The practical middle layer is integration testing that exercises business logic beneath the UI, giving most of the confidence of end-to-end testing without the browser. The test pyramid guide covers the layer proportions in detail.
When it stops paying off: as a literal ratio. The pyramid is a heuristic about relative cost and speed, not a quota, and treating it as one leads teams to write low-value unit tests to hit a shape.
The strategy pattern for distributed systems. Each consumer declares the response shape it depends on, and the provider verifies it can satisfy every declared contract, which catches integration breaks without standing up the full system.
It is the pattern that makes a narrow pyramid viable in a microservices estate, because it removes the argument that only end-to-end tests can prove services still talk to each other. See contract testing for the consumer-driven workflow, and microservices testing for how it fits alongside service and end-to-end layers.
When it stops paying off: in a monolith, or where one team owns both sides. The ceremony of publishing and verifying contracts only pays when consumer and provider ship on independent schedules.
Six recur most: the ice cream cone, over-mocking, brittle selectors, shared mutable state, assertions inside page objects, and hard-coded waits. Each has a specific fix.
| Anti-pattern | What it looks like | Fix |
|---|---|---|
| Ice cream cone | The pyramid inverted: most coverage sits in slow UI tests, with a thin unit layer underneath. | Push assertions down. Replace UI tests that verify business rules with service or unit tests that verify the same rule. |
| Over-mocking | Tests assert that a method was called rather than that the outcome is right, so refactoring breaks tests while behavior is unchanged. | Assert on returned state. Mock only what you do not own, such as third-party network calls. |
| Brittle selectors | Locators bound to auto-generated class names or absolute XPath that change whenever the markup is regenerated. | Address elements by role, label, or a dedicated test id, and keep them inside a page object. |
| Shared mutable state | Tests pass alone and fail in a suite because they depend on records another test created. | Give each test its own data through a builder, and restore state in the teardown phase. |
| Assertions in page objects | A page object that verifies as well as acts, so it cannot be reused by a test expecting a failure path. | Page objects act and expose state; tests assert. |
| Hard-coded waits | Fixed sleeps scattered through tests to paper over timing, which are simultaneously too slow and too short. | Use web-first assertions that retry until the condition holds. |
The ice cream cone is self-reinforcing. Slow suites get run less often, which lets defects through, which prompts more UI tests. Fowler's brittleness point is the exit: move the assertion down a layer rather than adding another test at the top. It hits regression testing hardest, because that is the suite that grows fastest.
Page Object Model and Screenplay carry frontend suites, Test Data Builder and Factory carry API testing suites, Contract Testing carries microservices, and Four-Phase Test matters most where teardown does real work. A pattern that carries a UI suite can be dead weight on an event-driven backend.
| Architecture | Patterns that carry the weight | What changes |
|---|---|---|
| Frontend and UI | Page Object Model, Screenplay, Fluent Interface | Addressing is the hard part, so most of the effort goes into keeping locators in one place and out of tests. |
| REST and API | Test Data Builder, Factory, Arrange-Act-Assert | There is no UI to model, so page objects disappear and request and payload construction becomes the thing worth abstracting. |
| Microservices | Contract Testing, Test Pyramid, Factory | Integration risk moves to service boundaries, so contracts replace most cross-service end-to-end tests. |
| Event-driven | Four-Phase Test, Test Data Builder, Contract Testing | Assertions become eventual rather than immediate, and explicit teardown matters more because consumers hold state between tests. |
| Database-backed | Four-Phase Test, Object Mother, Test Data Builder | The teardown phase does real work; wrapping each test in a transaction that rolls back keeps runs independent. |
The pattern that survives every architecture is Test Data Builder, because every layer needs valid objects with one field varied. The one most often applied where it does not belong is Page Object Model, which some teams carry into API suites as a "request object" wrapper that adds indirection without removing duplication.
Adopt a pattern in response to a symptom, never as upfront architecture. Match the pain you actually have against this table and adopt only the pattern that removes it.
| Symptom | Pattern to adopt |
|---|---|
| One markup change breaks dozens of tests | Page Object Model |
| Page objects have grown into unmanageable classes | Screenplay |
| Adding a required field breaks every test that builds that object | Test Data Builder |
| The same three or four fixtures recur everywhere | Object Mother |
| Tests read as a wall of setup with no visible intent | Arrange-Act-Assert |
| Product owners cannot review what is covered | Given-When-Then |
| Switching local to cloud execution means editing every test | Factory |
| The suite is slow and nobody trusts it | Test Pyramid |
| Services break each other between releases | Contract Testing |
For teams formalising this into a framework structure, the test automation architecture guide covers the layering. Patterns from general software design carry over too, as shown in the JavaScript design patterns guide and, for service estates, microservices design patterns.
Point the suite at a cloud grid. Patterns make a suite maintainable, not fast or broad, and a well-structured suite still runs against whatever browsers the machine holds. Because the Factory already centralises driver creation, moving execution is a configuration change rather than a rewrite.
TestMu AI's Automation Cloud runs existing Selenium, Cypress, Playwright, and Puppeteer scripts across 3,000+ real browser and OS combinations with no grid to maintain, and captures network logs, console logs, video, and screenshots on every session without extra configuration.
Two capabilities matter specifically for patterned suites. Auto Healing repairs broken locators as the UI evolves, which reduces the maintenance a page object exists to absorb. SmartWait waits for elements to become interactable, which removes the hard-coded waits listed in the anti-pattern table. Setup is covered in the Playwright testing documentation.
Once the suite runs on a grid, pipeline execution is the next step, covered in Playwright CI/CD and, for the layered strategy, shift-left testing.
Testing patterns pay off fastest when adopted one at a time. Pick the symptom that costs your team the most this month and adopt only the pattern that removes it. If a markup change breaks dozens of tests, that is Page Object Model, and it is a day of work.
Then audit for the ice cream cone. Count how many of your tests drive the UI to verify a rule that a service-level test could check, and move those down a layer, since that single change usually does more for suite speed than every structural pattern combined.
When maintenance is under control and coverage becomes the constraint, point the same suite at TestMu AI to run it across browser and operating system combinations no single machine holds.
Author
Bhavya Hada is a Community Contributor at TestMu AI with over three years of experience in software testing and quality assurance. She has authored 20+ articles on software testing, test automation, QA, and other tech topics. She holds certifications in Automation Testing, KaneAI, Selenium, Appium, Playwright, and Cypress. At TestMu AI, Bhavya leads marketing initiatives around AI-driven test automation and develops technical content across blogs, social media, newsletters, and community forums. On LinkedIn, she is followed by 4,000+ QA engineers, testers, and tech professionals.
Reviewer
Salman is a Test Automation Evangelist and Community Contributor at TestMu AI, with over 6 years of hands-on experience in software testing and automation. He has completed his Master of Technology in Computer Science and Engineering, demonstrating strong technical expertise in software development, testing, AI agents and LLMs. He is certified in KaneAI, Automation Testing, Selenium, Cypress, Playwright, and Appium, with deep experience in CI/CD pipelines, cross-browser testing, AI in testing, and mobile automation. Salman works closely with engineering teams to convert complex testing concepts into actionable, developer-first content. Salman has authored 120+ technical tutorials, guides, and documentation on test automation, web development, and related domains, making him a strong voice in the QA and testing community.
Did you find this page helpful?
More Related Blogs
TestMu AI forEnterprise
Get access to solutions built on Enterprise
grade security, privacy, & compliance