Page Objects vs. Functional Helpers
A while ago, I published Functional Programming Test Patterns with Cypress, where I went in-depth on why page objects are unnecessary in modern test automation. Back then, I didn’t realize just how ahead of its time that take was—since many people still insist on using page objects today.
This post is a simpler, updated version of that argument.
🚀 Why Functional Helpers > Page Objects?
The Page Object Model (POM) follows inheritance, while functional helpers follow composition.
But modern web apps are built with component-based architecture, where components are the real building blocks—not pages.
❓ If components compose and pages are just collections of them, does it really make sense to abstract pages with classes? Or does it introduce unnecessary duplication and over-abstraction?
In modern testing frameworks like Playwright and Cypress, strict Page Object Model (POM) is often overkill, especially when:
✅ You’re using data selectors (data-qa
, data-cy
) for stable locators.
✅ The tools already offer powerful built-in utilities for UI interactions.
✅ POM introduces extra complexity that makes debugging harder.
❌ Why Page Objects No Longer Make Sense
1️⃣ Unnecessary Abstraction
- POM adds an extra layer that often doesn’t provide feasible value.
- Modern test frameworks are already powerful enough without it.
2️⃣ Base Page Inheritance is Overkill
- Having a
BasePage
class with generic methods (click()
,fill()
) just to wrap Playwright’s API (or Cypress) makes no sense. - Playwright (or Cy) already has
page.locator()
,page.click()
,page.fill()
, etc.
3️⃣ Harder Debugging
- With POM, if a test fails, you have to jump between multiple files to figure out what went wrong.
- With direct helper functions, you see exactly what’s happening.
🔴 Traditional Page Object Model (POM)
🚨 Problems with POM:
❌ Unnecessary complexity → Extra class & inheritance
❌ Harder debugging → Need to jump between files
❌ Wrapping Playwright’s own API for no reason
🔹 Example (LoginPage.js
- POM Approach)
class LoginPage {
constructor(page) {
this.page = page;
this.usernameField = page.locator('[data-testid="username"]');
this.passwordField = page.locator('[data-testid="password"]');
this.loginButton = page.locator('[data-testid="login-button"]');
}
async login(username, password) {
await this.usernameField.fill(username);
await this.passwordField.fill(password);
await this.loginButton.click();
}
}
export default LoginPage;
🔹 Usage in a Test
import { test, expect } from '@playwright/test';
import LoginPage from './LoginPage.js';
test('User can log in', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login('testUser', 'password123');
await expect(page.locator('[data-testid="welcome-message"]')).toHaveText('Welcome, testUser');
});
✅ Functional Helper Approach (Better)
📌 Why is this better?
✅ No extra class → Directly use Playwright API
✅ No unnecessary this.page
assignments
✅ Much easier to maintain & debug
🔹 Example (loginHelpers.js
- Functional Helper Approach)
export async function login(page, username, password) {
await page.fill('[data-testid="username"]', username);
await page.fill('[data-testid="password"]', password);
await page.click('[data-testid="login-button"]');
}
🔹 Usage in a Test
import { test, expect } from '@playwright/test';
import { login } from './loginHelpers.js';
test('User can log in', async ({ page }) => {
await login(page, 'testUser', 'password123');
await expect(page.locator('[data-testid="welcome-message"]')).toHaveText('Welcome, testUser');
});
🔥 Final Thoughts
Helper functions are simpler, faster to debug, and scale better in component-driven apps.
💡 POM was useful in Selenium/WebDriver days, but today? Just use functions.
🔥 What do you think? Are you still using POM? Have you already switched to functional helpers?
💬 Drop a comment below—I’d love to hear your take on this!
Addendum
Thanks everyone for their contributions!
One thing I identified is that Component in testing isn’t used like in UI frameworks.
For frontend devs, a component is a reusable UI unit with state, props, and lifecycle hooks.
For QA, a component is often just a logical grouping of elements & interactions within a larger page.
🔹 ex: A checkout form in testing might represent the entire checkout UI, while in React, it might be broken into FormField, Button, AddressInput.
🔹 In POM, subcomponents are often instantiated as properties within a page object, mirroring the UI structure but without true reusability.
While I understand this approach, I don’t fully agree with the terminology.
I advocate for UI-component-driven testing, as I discuss in my book: https://muratkerem.gitbook.io/cctdd.
With tools like PW Component Testing, Cy Component Testing & Vitest with UI, we can now test at a lower level, reducing the need for full-page interactions.
Though still uncommon in QA, this shift solves many POM complexities:
✅ test components in isolation before e2e
✅ many cases covered at the UI component level
✅ smaller tests = quicker execution
We only move up the testing pyramid when we must: routing, user flows, backend calls.
I see that even the strongest advocates of POM now acknowledge its role has shifted—it’s mainly used to organize and store locators. But with AI-assisted test debugging advancing rapidly, it’s time to rethink this approach entirely.
Watch this Playwright demo ~13 min mark by Debbie O'Brien & Simon Knott
They showcase an experimental Playwright config that outputs the DOM in a structured way, similar to aria snapshots:
This creates a clean, machine-readable format (like YML) that represents the DOM in a concise way. The AI then analyzes the DOM snapshot, diagnoses the failure, and suggests stable ARIA selectors.
What does this mean for QA?
Instead of manually managing locators in a POM abstraction, we should adopt ARIA selectors as the standard for DOM interaction.
1.ARIA selectors provide a shared understanding between screen readers, test tools like Playwright, and AI.
2.Debugging can be AI-powered—rather than relying on a static POM file, AI can analyze the ARIA selector tree and suggest better selectors dynamically.
3.The DOM becomes the source of truth, not a manually maintained POM abstraction
If the argument is still “store all selectors in a POM,” I disagree. The future of test automation is ARIA-driven, where the source code and AI-powered ARIA snapshots become the true source of truth—not a manually curated selectors file.
Top comments (16)
Nice write-up.
I still prefer POMs - I like having 1-1 abstractions between the component and a testing flow. Each component gets a POM, and subcomponents are instantiated as POM properties. It makes the mental model easier for me to reason about as things scale. With functional approaches, I find people tend to be unsure which files to put functions, and in a team environment it can get fractured and confusing.
I use TypeScript so the constructor property setting gets rid of the extra assignment work, I still use Playwright's API directly, and the benefit of the defined 1-1 rule outweighs the extra bloat that a class structure may create.
Nonetheless appreciate the well-articulated perspective!
The TS constructor shortcut is a good point, I always use it as well.
One thing I identified from your post, and correlated with the information I get from other QA is that, the term "Component" is not used in the same context as a React/Angular/Vue as developers think.
For frontend devs, a component is a reusable UI unit with state, props, and lifecycle hooks.
For test automation, a component is often just a logical grouping of elements & interactions within a larger page.
• Example: A checkout form component in testing might represent the entire checkout UI, while in React, it might be broken into FormField, Button, AddressInput, etc.
• In POM, this means subcomponents are typically instantiated as properties within a page object, mirroring how a UI is structured for interaction rather than strict reusability.
While I understand the reasoning behind this, I don’t fully agree with the terminology.
I personally advocate for UI-component-driven testing, as I discuss in my bookhttps://muratkerem.gitbook.io/cctdd .
With modern tools like Playwright Component Testing, Cypress Component Testing, and Vitest with UI, we can now test at a lower level, making full-page interactions less necessary.
It is an alien approach for QA at the moment, but this shift indirectly solves many POM complexities:
We only move up the pyramid when there are things we cannot fully test at components, like routing, user flows, backend calls.
I've started started diving into the vue test tools and I struggle to understand how to use it effectively.
I try to look at tests ensuring code can be refractored. And vue is very declarative which leeds to tests that look to verify the code generator which I see as mostly wasted.
When devs write vue components what are their mistakes and refactoring that happens?
A good point view! I'd like to share my point too. I have a nice example of avoiding POM approach and just focusing on functions in e2e scenarios. Sooner or later the code becomes unbearable stack of lines and you need to handle it somehow so it stays readable (it all depends on dev's experience, anyway, or could use test blocks, steps, comments etc.). As for me POM has not only inheritance, but also a lexical context, as it was mentioned in previous comments. Like, you create on one page, then view on another and delete on the third. All these actions will be faceless until you start to wrap and name it like, createAItem, createBItem, createCItem, the same for view and delete actions, what seems like easier to be done in terms of different classes. Also don't forget about Playwright's fixture approach that ease the process class instantiation. So, as for me function helpers are suitable for a one functionality test cases or something like this. For complex or integration test cases POM is fitting better, I assume. Any thoughts on that case?
I see your point.
Ironically one way to get around that is using fixtures without classes. Take a look at this repo github.com/muratkeremozcan/cy-vs-p... .
Here is something you can improve POM with youtube.com/watch?v=of1v9cycTdQ
Decorators in POM methods.
Again, ironically, the same tweak can be used with the functional approach to solve the problem you identified.
Cool-cool! Thanks for examples!
I don't think this addresses real issues with POM. There are a few challenges that you'd want to tackle.
POM recommendation included hiding framework interaction, in the context of selenium your test shouldn't touch IWebElement. I disagree with this approach for abstraction.
But you have replaced classes with modules, which is fine, but not a real difference.
Objects still provide guidance, where the login operation can return the welcome page where available locators are defined. If all you are doing with objects is filling out boilerplate then sure there is no value.
The one I like is click delete it returns an object representing the confirm dialog.
Appreciate your perspective. This discussion has been happening across many responses, and I covered my stance in the main post. The shift I’m advocating is moving beyond manually organizing locators—leveraging AI-assisted debugging and ARIA selectors as the new standard.
Hi Murat,
Thanks for sharing this post, I'm a big fan of both your QA articles and courses on Udemy. 🤠
I've a question regarding using Functional Programming instead of POM, if we have to automate a flow scenario, for example checkout a product in an E-commerce web app, and we need to navigate between pages, in POM I had to create classes with Page Objects then create instances of these classes and use them into the test file, if I will use the functional helpers pattern, how can I achieve this?
Simple: just compose functions—no need for classes!
🔥 TL;DR
• If a flow is one-off? Just write UI actions directly.
• If reused? Use functional helpers—not Page Objects.
❌ Unnecessary classes
❌ Extra abstraction
❌ Harder debugging
This is what I prefer, if the code isn't repeated anywhere else
✅ No class instantiation
✅ No unnecessary helpers
✅ Just readable, direct UI actions
✅ If this flow is repeated, use functional helpers instead
Hi Murat,
Thank you for sharing your opinion.
I really like the idea of the functional helpers as I am moving towards Playwright from Java/Selenium for my new projects. It does make sense that POM is not entirely necessary in such new tools. However I find it hard for the new tools to maintain locators as in your examples, you might need to repeat the locator strategy in many functions (e.g: Input text in textbox, verify text in textbox). If the UI changes, then we need to find all functions to update instead updating 1 line of code in POM.
Can you help me share the way we can deal with this problem if we implement functional helpers?
Thank you again!
Hi Duy,
What I recommend for locators is adding selectors in the source code, or using testing-library-like semantic selectors in PW (which come built-in).
Take if you check out my Udemy courses, this is the way I did it there udemy.com/user/murat-ozcan-14/?srs...
Here is the repo you can take a look at github.com/muratkeremozcan/pact-js.... Look for data-cy selectors. You will also find testing-library-like selectors where data-cy isn't used.
Cheers,
Murat
Technically Pom can have composition too and still be class based
If we need another one method to enter username alone for sake of field validation we need to paste along with locator for Functional helper whereas for POM locator will be in one place. Always POM
You don’t need POM to centralize locators—just use a locators module instead.
If you must do this (and I'm not a fan) you can apply something like the below.
This looks great