Page Transactions and Page Object Model

Page Transactions pattern for testing automation (2 Part Series)

1 Page Transactions as a new way to organize your testing automation
2 Page Transactions and Page Object Model

Recently I wrote Page Transactions as a new way to organize your testing automation, focusing on simplicity, readability and flexibility. Now, I present a structured comparison of Page Transactions (PT) and Page Objects Model (POM) explaining the pros and cons of each pattern. At the end I give some tips to migrate from POM to PT in simple steps.


To whom that is not familiar with POM, in a nutshell, it is a design pattern used in testing automation where the objects on a UI page are represented as methods in a class and where each class represents the UI page itself.

The comparison takes into account aspects like maintainability, reusability, readability and simplicity. Lets start with POM which is a popular pattern in test automation.

Page Objects Model

Code maintainability

Separates locators from tests Easy to update elements

Still tightly coupled with page structure If UI changes, all affected pages must be updated

Reusability

Page classes can be reused across multiple tests

Interactions are page-specific, not user-flow based

Readability and Simplicity

Test cases look clean with method calls

Requires writing multiple classes for different pages, which can add complexity

Scalability

Works well for large projects with many pages

Harder to manage when dealing with workflows spanning multiple pages

Test maintenance effort

If a locator changes, it only needs to be updated in one place

If the user workflow changes, tests might need significant updates

Suitability for UI testing

Well-suited for UI automation with Selenium and similar tools

Not ideal for non-UI scenarios (e.g., REST API testing)


Page Transactions

Code maintainability

Transactions focus on user actions rather than elements, reducing maintenance

Requires a different mindset for those used to POM

Reusability

Highly reusable since transactions are user-action based

If UI structure changes drastically, transactions might need updates

Readability & Simplicity

Tests read like human instructions, improving clarity Reduces WebDriver calls in test script

Requires defining multiple transaction classes for different actions

Scalability

Works well for large applications, especially where user flows span multiple pages

Slightly more setup is needed compared to POM

Test maintenance effort

Less maintenance required since tests are written based on actions rather than UI elements

If a transaction is reused across many tests, fixing one issue could impact multiple tests

Suitability for UI testing

Works for UI, API, and asynchronous testing

Less common in traditional UI automation frameworks


Analysis

PT focuses on user workflows, making it more maintainable and scalable. It allows for better test organization and is more suitable for different types of automation (Web UI, API, Desktop UI). Tests written in PT are more readable, making them accessible to non-technical stakeholders. If your project involves complex user workflows across multiple pages, Page Transactions is the better approach.

POM is still a great choice for teams already familiar with traditional UI testing methods, and it provides a structured way to manage page elements. If you need simple UI structure representation, POM works well.

So, no pattern is best or worst. It depends on the characteristics of a given project. However, if the project fits better with PT pattern, but is already implemented in POM, it is possible to migrate from POM to PT following these tips.


Tips for Migrating from POM to PT

Moving from POM to PT requires shifting from page-centric automation to user action-centric automation. Below are key steps and examples to help with the transition.

1 Identify User Transactions: In POM, page objects represent pages. In PT, it focus on transactions like Login, Logout, Submit Form, Change Language, Navigate to a Page. Instead of writing separate methods for every element on a page, create classes for each transaction.

2 Convert POM Methods to PT Transactions: Check bellow a POM Example (Before Migration). The test login to a page, informs the user and password of a user and checks if the Dashboard page is displayed. There is a LoginPage class which hosts the methods to interact with the components of the page, like username field.

<span>from</span> <span>selenium.webdriver.common.by</span> <span>import</span> <span>By</span>
<span>class</span> <span>LoginPage</span><span>:</span>
<span>def</span> <span>__init__</span><span>(</span><span>self</span><span>,</span> <span>driver</span><span>):</span>
<span>self</span><span>.</span><span>driver</span> <span>=</span> <span>driver</span>
<span>self</span><span>.</span><span>username</span> <span>=</span> <span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>username</span><span>"</span><span>)</span>
<span>self</span><span>.</span><span>password</span> <span>=</span> <span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>password</span><span>"</span><span>)</span>
<span>self</span><span>.</span><span>login_button</span> <span>=</span> <span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>login</span><span>"</span><span>)</span>
<span>def</span> <span>enter_username</span><span>(</span><span>self</span><span>,</span> <span>user</span><span>):</span>
<span>self</span><span>.</span><span>driver</span><span>.</span><span>find_element</span><span>(</span><span>*</span><span>self</span><span>.</span><span>username</span><span>).</span><span>send_keys</span><span>(</span><span>user</span><span>)</span>
<span>def</span> <span>enter_password</span><span>(</span><span>self</span><span>,</span> <span>pwd</span><span>):</span>
<span>self</span><span>.</span><span>driver</span><span>.</span><span>find_element</span><span>(</span><span>*</span><span>self</span><span>.</span><span>password</span><span>).</span><span>send_keys</span><span>(</span><span>pwd</span><span>)</span>
<span>def</span> <span>click_login</span><span>(</span><span>self</span><span>):</span>
<span>self</span><span>.</span><span>driver</span><span>.</span><span>find_element</span><span>(</span><span>*</span><span>self</span><span>.</span><span>login_button</span><span>).</span><span>click</span><span>()</span>
<span>from</span> <span>selenium.webdriver.common.by</span> <span>import</span> <span>By</span>

<span>class</span> <span>LoginPage</span><span>:</span>
    <span>def</span> <span>__init__</span><span>(</span><span>self</span><span>,</span> <span>driver</span><span>):</span>
        <span>self</span><span>.</span><span>driver</span> <span>=</span> <span>driver</span>
        <span>self</span><span>.</span><span>username</span> <span>=</span> <span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>username</span><span>"</span><span>)</span>
        <span>self</span><span>.</span><span>password</span> <span>=</span> <span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>password</span><span>"</span><span>)</span>
        <span>self</span><span>.</span><span>login_button</span> <span>=</span> <span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>login</span><span>"</span><span>)</span>

    <span>def</span> <span>enter_username</span><span>(</span><span>self</span><span>,</span> <span>user</span><span>):</span>
        <span>self</span><span>.</span><span>driver</span><span>.</span><span>find_element</span><span>(</span><span>*</span><span>self</span><span>.</span><span>username</span><span>).</span><span>send_keys</span><span>(</span><span>user</span><span>)</span>

    <span>def</span> <span>enter_password</span><span>(</span><span>self</span><span>,</span> <span>pwd</span><span>):</span>
        <span>self</span><span>.</span><span>driver</span><span>.</span><span>find_element</span><span>(</span><span>*</span><span>self</span><span>.</span><span>password</span><span>).</span><span>send_keys</span><span>(</span><span>pwd</span><span>)</span>

    <span>def</span> <span>click_login</span><span>(</span><span>self</span><span>):</span>
        <span>self</span><span>.</span><span>driver</span><span>.</span><span>find_element</span><span>(</span><span>*</span><span>self</span><span>.</span><span>login_button</span><span>).</span><span>click</span><span>()</span>
from selenium.webdriver.common.by import By class LoginPage: def __init__(self, driver): self.driver = driver self.username = (By.ID, "username") self.password = (By.ID, "password") self.login_button = (By.ID, "login") def enter_username(self, user): self.driver.find_element(*self.username).send_keys(user) def enter_password(self, pwd): self.driver.find_element(*self.password).send_keys(pwd) def click_login(self): self.driver.find_element(*self.login_button).click()

Enter fullscreen mode Exit fullscreen mode

<span>def</span> <span>test_login</span><span>():</span>
<span>driver</span> <span>=</span> <span>webdriver</span><span>.</span><span>Chrome</span><span>()</span>
<span>login_page</span> <span>=</span> <span>LoginPage</span><span>(</span><span>driver</span><span>)</span>
<span>login_page</span><span>.</span><span>enter_username</span><span>(</span><span>"</span><span>user1</span><span>"</span><span>)</span>
<span>login_page</span><span>.</span><span>enter_password</span><span>(</span><span>"</span><span>password1</span><span>"</span><span>)</span>
<span>login_page</span><span>.</span><span>click_login</span><span>()</span>
<span>assert</span> <span>"</span><span>Dashboard</span><span>"</span> <span>in</span> <span>driver</span><span>.</span><span>title</span>
<span>def</span> <span>test_login</span><span>():</span>
    <span>driver</span> <span>=</span> <span>webdriver</span><span>.</span><span>Chrome</span><span>()</span>
    <span>login_page</span> <span>=</span> <span>LoginPage</span><span>(</span><span>driver</span><span>)</span>

    <span>login_page</span><span>.</span><span>enter_username</span><span>(</span><span>"</span><span>user1</span><span>"</span><span>)</span>
    <span>login_page</span><span>.</span><span>enter_password</span><span>(</span><span>"</span><span>password1</span><span>"</span><span>)</span>
    <span>login_page</span><span>.</span><span>click_login</span><span>()</span>

    <span>assert</span> <span>"</span><span>Dashboard</span><span>"</span> <span>in</span> <span>driver</span><span>.</span><span>title</span>
def test_login(): driver = webdriver.Chrome() login_page = LoginPage(driver) login_page.enter_username("user1") login_page.enter_password("password1") login_page.click_login() assert "Dashboard" in driver.title

Enter fullscreen mode Exit fullscreen mode

PT Example (After Migration). Now, the LoginTransaction class represents the whole login action which adds the user name, password and submit them.

<span>from</span> <span>guara.transaction</span> <span>import</span> <span>AbstractTransaction</span>
<span>class</span> <span>LoginTransaction</span><span>(</span><span>AbstractTransaction</span><span>):</span>
<span>def</span> <span>__init__</span><span>(</span><span>self</span><span>,</span> <span>driver</span><span>):</span>
<span>super</span><span>().</span><span>__init__</span><span>(</span><span>driver</span><span>)</span>
<span>def</span> <span>do</span><span>(</span><span>self</span><span>,</span> <span>username</span><span>,</span> <span>password</span><span>):</span>
<span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>username</span><span>"</span><span>).</span><span>send_keys</span><span>(</span><span>username</span><span>)</span>
<span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>password</span><span>"</span><span>).</span><span>send_keys</span><span>(</span><span>password</span><span>)</span>
<span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>login</span><span>"</span><span>).</span><span>click</span><span>()</span>
<span>return</span> <span>self</span><span>.</span><span>_driver</span><span>.</span><span>title</span>
<span>from</span> <span>guara.transaction</span> <span>import</span> <span>AbstractTransaction</span>

<span>class</span> <span>LoginTransaction</span><span>(</span><span>AbstractTransaction</span><span>):</span>
    <span>def</span> <span>__init__</span><span>(</span><span>self</span><span>,</span> <span>driver</span><span>):</span>
        <span>super</span><span>().</span><span>__init__</span><span>(</span><span>driver</span><span>)</span>

    <span>def</span> <span>do</span><span>(</span><span>self</span><span>,</span> <span>username</span><span>,</span> <span>password</span><span>):</span>
        <span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>username</span><span>"</span><span>).</span><span>send_keys</span><span>(</span><span>username</span><span>)</span>
        <span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>password</span><span>"</span><span>).</span><span>send_keys</span><span>(</span><span>password</span><span>)</span>
        <span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>login</span><span>"</span><span>).</span><span>click</span><span>()</span>
        <span>return</span> <span>self</span><span>.</span><span>_driver</span><span>.</span><span>title</span>
from guara.transaction import AbstractTransaction class LoginTransaction(AbstractTransaction): def __init__(self, driver): super().__init__(driver) def do(self, username, password): self._driver.find_element(By.ID, "username").send_keys(username) self._driver.find_element(By.ID, "password").send_keys(password) self._driver.find_element(By.ID, "login").click() return self._driver.title

Enter fullscreen mode Exit fullscreen mode

<span>from</span> <span>guara.transaction</span> <span>import</span> <span>Application</span>
<span>from</span> <span>guara</span> <span>import</span> <span>it</span>
<span>from</span> <span>selenium</span> <span>import</span> <span>webdriver</span>
<span>def</span> <span>test_login</span><span>():</span>
<span>driver</span> <span>=</span> <span>webdriver</span><span>.</span><span>Chrome</span><span>()</span>
<span>app</span> <span>=</span> <span>Application</span><span>(</span><span>driver</span><span>)</span>
<span>app</span><span>.</span><span>at</span><span>(</span><span>LoginTransaction</span><span>,</span> <span>username</span><span>=</span><span>"</span><span>user1</span><span>"</span><span>,</span> <span>password</span><span>=</span><span>"</span><span>password1</span><span>"</span><span>).</span><span>asserts</span><span>(</span>
<span>it</span><span>.</span><span>Contains</span><span>,</span> <span>"</span><span>Dashboard</span><span>"</span>
<span>)</span>
<span>from</span> <span>guara.transaction</span> <span>import</span> <span>Application</span>
<span>from</span> <span>guara</span> <span>import</span> <span>it</span>
<span>from</span> <span>selenium</span> <span>import</span> <span>webdriver</span>

<span>def</span> <span>test_login</span><span>():</span>
    <span>driver</span> <span>=</span> <span>webdriver</span><span>.</span><span>Chrome</span><span>()</span>
    <span>app</span> <span>=</span> <span>Application</span><span>(</span><span>driver</span><span>)</span>

    <span>app</span><span>.</span><span>at</span><span>(</span><span>LoginTransaction</span><span>,</span> <span>username</span><span>=</span><span>"</span><span>user1</span><span>"</span><span>,</span> <span>password</span><span>=</span><span>"</span><span>password1</span><span>"</span><span>).</span><span>asserts</span><span>(</span>
      <span>it</span><span>.</span><span>Contains</span><span>,</span> <span>"</span><span>Dashboard</span><span>"</span>
    <span>)</span>
from guara.transaction import Application from guara import it from selenium import webdriver def test_login(): driver = webdriver.Chrome() app = Application(driver) app.at(LoginTransaction, username="user1", password="password1").asserts( it.Contains, "Dashboard" )

Enter fullscreen mode Exit fullscreen mode

Benefits of the PT Approach

  • Less boilerplate code as no needs for page-specific methods in tests
  • Readable test cases once tests describe actions in plain English
  • Easier maintenance as UI changes don’t break all related test methods

3 Additional Refactoring for Other Pages: for example changing Language Transaction, instead of:

<span>class</span> <span>HomePage</span><span>:</span>
<span>def</span> <span>click_language_button</span><span>(</span><span>self</span><span>):</span>
<span>self</span><span>.</span><span>driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>language</span><span>"</span><span>).</span><span>click</span><span>()</span>
<span>class</span> <span>HomePage</span><span>:</span>
    <span>def</span> <span>click_language_button</span><span>(</span><span>self</span><span>):</span>
        <span>self</span><span>.</span><span>driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>"</span><span>language</span><span>"</span><span>).</span><span>click</span><span>()</span>
class HomePage: def click_language_button(self): self.driver.find_element(By.ID, "language").click()

Enter fullscreen mode Exit fullscreen mode

Use

<span>class</span> <span>ChangeLanguageTransaction</span><span>(</span><span>AbstractTransaction</span><span>):</span>
<span>def</span> <span>__init__</span><span>(</span><span>self</span><span>,</span> <span>driver</span><span>):</span>
<span>super</span><span>().</span><span>__init__</span><span>(</span><span>driver</span><span>)</span>
<span>def</span> <span>do</span><span>(</span><span>self</span><span>,</span> <span>language</span><span>):</span>
<span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>f</span><span>"</span><span>lang-</span><span>{</span><span>language</span><span>}</span><span>"</span><span>).</span><span>click</span><span>()</span>
<span>return</span> <span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>TAG_NAME</span><span>,</span> <span>"</span><span>body</span><span>"</span><span>).</span><span>text</span>
<span>class</span> <span>ChangeLanguageTransaction</span><span>(</span><span>AbstractTransaction</span><span>):</span>
    <span>def</span> <span>__init__</span><span>(</span><span>self</span><span>,</span> <span>driver</span><span>):</span>
        <span>super</span><span>().</span><span>__init__</span><span>(</span><span>driver</span><span>)</span>

    <span>def</span> <span>do</span><span>(</span><span>self</span><span>,</span> <span>language</span><span>):</span>
        <span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>ID</span><span>,</span> <span>f</span><span>"</span><span>lang-</span><span>{</span><span>language</span><span>}</span><span>"</span><span>).</span><span>click</span><span>()</span>
        <span>return</span> <span>self</span><span>.</span><span>_driver</span><span>.</span><span>find_element</span><span>(</span><span>By</span><span>.</span><span>TAG_NAME</span><span>,</span> <span>"</span><span>body</span><span>"</span><span>).</span><span>text</span>
class ChangeLanguageTransaction(AbstractTransaction): def __init__(self, driver): super().__init__(driver) def do(self, language): self._driver.find_element(By.ID, f"lang-{language}").click() return self._driver.find_element(By.TAG_NAME, "body").text

Enter fullscreen mode Exit fullscreen mode

Test case:

<span>app</span><span>.</span><span>at</span><span>(</span><span>ChangeLanguageTransaction</span><span>,</span> <span>language</span><span>=</span><span>"</span><span>pt</span><span>"</span><span>).</span><span>asserts</span><span>(</span>
<span>it</span><span>.</span><span>Contains</span><span>,</span> <span>"</span><span>Página inicial</span><span>"</span>
<span>)</span>
<span>app</span><span>.</span><span>at</span><span>(</span><span>ChangeLanguageTransaction</span><span>,</span> <span>language</span><span>=</span><span>"</span><span>pt</span><span>"</span><span>).</span><span>asserts</span><span>(</span>
  <span>it</span><span>.</span><span>Contains</span><span>,</span> <span>"</span><span>Página inicial</span><span>"</span>
<span>)</span>
app.at(ChangeLanguageTransaction, language="pt").asserts( it.Contains, "Página inicial" )

Enter fullscreen mode Exit fullscreen mode

4 Gradual Migration Strategy:

  • Start with frequently changing tests
  • Convert one transaction at a time
  • Keep old POM code until full migration is complete

Conclusion

PT is a new pattern, but demonstrates good qualities when compared against POM that is a widely used pattern in test automation. It in some situations PT fits better and can be used as an alternative to POM.

Page Transactions pattern for testing automation (2 Part Series)

1 Page Transactions as a new way to organize your testing automation
2 Page Transactions and Page Object Model

原文链接:Page Transactions and Page Object Model

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
A good idea without action is worth nothing.
如果没有切实执行,再好的点子也是徒劳
评论 抢沙发

请登录后发表评论

    暂无评论内容