The Page Object Model: Organizing Your UI Tests

Our test script from the last lesson is now robust. It's patient. It handles the asynchronous nature of the web. But it has a growing architectural problem. All of our locators and interaction logic are tangled up directly within our test methods. It's a maintenance bottleneck waiting to happen.

Imagine the application's login button ID changes from login-button to submit-login. You would have to hunt down and update that locator in every single test file that performs a login. Now multiply that by hundreds of tests and dozens of UI changes. This is how test suites die a slow, painful death by a thousand cuts.

This lesson introduces the elegant solution to this chaos. The Page Object Model (POM) is not a tool or a library; it's a design philosophy that will transform your code from a collection of brittle scripts into a clean, scalable, and professional test automation framework. 🏛️

Tommy and Gina are working in the lab amidst piles of pages

Anatomy of a Brittle Test Script

Let's perform a code review on the test we built in the 02-robust-interactions project. While functional, it violates several key software design principles.

The Architectural Flaws

  • No Abstraction: The test is deeply concerned with the how of the UI (e.g., By.Id("user-name"), .SendKeys()). A good test should only care about the what (e.g., "log in as a standard user"). The implementation details are noise.
  • High Duplication: If we write ten more tests that require logging in, we will copy and paste the same five lines of Selenium code ten times. This is a direct violation of the "Don't Repeat Yourself" (DRY) principle.
  • Scattered Locators: The locators (our By.Id calls) are hardcoded directly in the test method. A single UI change forces us to search through all our test files, leading to a massive and error-prone refactoring effort.

Think of this problem in terms of maintaining a house. Without proper organization, you'd leave your tools scattered across every room—wrenches under the couch, screwdrivers in the freezer. So when something breaks, you waste time searching before you can even start fixing it. Our current test structure works the same way: UI knowledge is scattered throughout individual test methods, so any small change requires a frustrating scavenger hunt through brittle code.

The Solution: Page Object Model

The Page Object Model (POM) is an object-oriented design pattern where each page of your application is represented by a corresponding C# class. This class becomes the single, authoritative source of truth for that page's UI elements and the interactions a user can perform on them.

The Page Object Model acts like a well-organized toolkit in a dedicated repair room. Each page in your app gets its own tool drawer, labeled and stocked with the exact instruments needed. So when the login button changes from login-button to submit-login, you're not rifling through dozens of tests—you're simply updating the drawer labeled "Login Page". With this abstraction, test scripts stop micromanaging every step like "turn screwdriver, loosen bolt" and instead say "replace faucet". That clarity doesn't just clean up your code; it makes your entire automation framework scalable and resilient to change.

The Two Golden Rules of POM

  1. A Page Object Encapsulates the UI. The Page Object class is the only place that should contain Selenium WebDriver code (FindElement(), Click(), etc.). It holds all the locators for the page's elements and exposes high-level, business-facing methods that represent user workflows (e.g., a Login(string username, string password) method).
  2. A Test Script Dictates the Business Flow. The test method should contain zero direct calls to the Selenium API. Its only job is to instantiate Page Objects and call their methods to simulate a user's journey, then use assertions to verify the outcome.

This creates a powerful layer of abstraction between your test logic and the underlying implementation of the UI, as shown below.

classDiagram class TestLogic { -Test Cases -Assertions +ExecuteTests() +VerifyResults() } class PageObject { -WebElements -BrowserInteractions +NavigateToPage() +InteractWithElements() +GetElementState() } class UILogic { -Browser -DOM +RenderPage() +HandleEvents() } TestLogic --> PageObject : Uses PageObject --> UILogic : Interacts with note for PageObject "Acts as a bridge
between TestLogic
and UILogic"
The Page Object acts as a bridge between the test and the browser.

The Mental Model: Thinking in Layers

Before we write code, let's establish the correct mental model for how Page Objects work. Imagine your test automation framework as a three-layer cake:

Layer 1 - Test Layer (Top): This is where your business logic lives. Tests at this layer read like user stories: "As a customer, I want to log in and add items to my cart." This layer knows nothing about HTML, CSS selectors, or WebDriver methods.

Layer 2 - Page Object Layer (Middle): This is your translation layer. It converts high-level business actions ("login as standard user") into specific UI interactions ("find username field, enter text, find password field, enter text, click login button"). This layer understands both business concepts and technical implementation.

Layer 3 - WebDriver Layer (Bottom): This is pure technical implementation. WebDriver handles the actual browser automation, element finding, and user input simulation. Your Page Objects interact with this layer, but your tests never do directly.

This layered approach provides flexibility and maintainability. When the UI changes, you only modify Layer 2. When business requirements change, you only modify Layer 1. Each layer has a single, clear responsibility.

Beyond Pages: The Rise of Component Objects

The name "Page Object Model" is a bit of a historical artifact. Modern web applications, built with frameworks like React, Angular, or Vue, are often composed of reusable, independent components rather than monolithic pages.

Consider a website's header. It appears on almost every page and contains the main navigation, a search bar, and a shopping cart icon. Instead of duplicating the locators and methods for the header in every single Page Object (HomePage, ProductPage, ContactPage), a more advanced approach is to create a HeaderComponent object.

This HeaderComponent would encapsulate all the elements and interactions for just the header. Then, any Page Object that includes the header can simply contain an instance of the HeaderComponent. This makes your framework even more modular, reusable, and aligned with modern development practices.

The Principle: Your test automation architecture should mirror your application's architecture. If the app is built from components, your framework should be too.

Building Your First Page Object

Let's translate the theory into code. We will create a LoginPage.cs class that represents the login page of SauceDemo. You can find this implementation in the 03-page-objects folder of the course repository.

As we build this class together, notice how each element serves a specific architectural purpose. We're not just organizing code for the sake of organization—we're creating a maintainable, scalable foundation for our test suite.

The Anatomy of a Page Object Class

  1. Driver Field: A private, readonly IWebDriver field to hold the browser instance.
  2. Wait Field: A private WebDriverWait field for robust element interactions.
  3. Constructor: A public constructor that accepts an IWebDriver instance and initializes both driver and wait fields.
  4. Locators: Private By fields defined at the top of the class. This centralizes all locators for the page in one easy-to-find place.
  5. Element Properties: Public C# properties for each element that use a "fat arrow" expression (=>) to find the element on demand with explicit waits.
  6. Service Methods: Public methods that represent user workflows. These methods contain the actual Selenium interaction logic with proper wait conditions.
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
 
namespace PageObjects
{
    public class LoginPage
    {
        // 1. Private fields to store WebDriver and WebDriverWait instances
        private readonly IWebDriver _driver;
        private readonly WebDriverWait _wait;
 
        // 2. Constructor to initialize the WebDriver and WebDriverWait
        public LoginPage(IWebDriver driver)
        {
            _driver = driver;
            _wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
        }
 
        // 3. Private By locators for all elements on the page
        private readonly By _usernameLocator = By.Id("user-name");
        private readonly By _passwordLocator = By.Id("password");
        private readonly By _loginButtonLocator = By.Id("login-button");
        private readonly By _errorMessageLocator = By.CssSelector("[data-test='error']");
 
        // 4. Public properties with explicit waits for robust element access
        public IWebElement UsernameInput => _wait.Until(d => d.FindElement(_usernameLocator));
        public IWebElement PasswordInput => _driver.FindElement(_passwordLocator);
        public IWebElement LoginButton => _driver.FindElement(_loginButtonLocator);
        public IWebElement ErrorMessage => _wait.Until(d => d.FindElement(_errorMessageLocator));
 
        // 5. Navigation method with wait for page load
        public void NavigateTo()
        {
            _driver.Navigate().GoToUrl("https://www.saucedemo.com/");
            // Wait for page to load by ensuring username field is present
            _wait.Until(d => d.FindElement(_usernameLocator));
        }
    }
}

Advanced Page Object Patterns

Now that you understand the basic structure, let's explore some advanced patterns that will make your Page Objects even more powerful and maintainable. These patterns address the limitations we encountered in our previous lesson's script.

The Factory Pattern for Page Navigation

One common challenge in Page Object design is handling navigation between pages. When a user clicks "Login", how does the test know it should now work with a ProductsPage instead of a LoginPage? The Factory pattern provides an elegant solution while incorporating robust wait conditions.

public class LoginPage
{
    // ... existing code ...
 
    // This method returns a ProductsPage, indicating successful navigation
    public ProductsPage LoginSuccessfully(string username, string password)
    {
        UsernameInput.SendKeys(username);
        PasswordInput.SendKeys(password);
        LoginButton.Click();
 
        // Wait for navigation to complete by checking for inventory container
        _wait.Until(driver => driver.FindElement(By.Id("inventory_container")));
 
        return new ProductsPage(_driver);
    }
 
    // This method stays on the same page for failed login attempts
    public LoginPage LoginUnsuccessfully(string username, string password)
    {
        UsernameInput.SendKeys(username);
        PasswordInput.SendKeys(password);
        LoginButton.Click();
 
        // Wait for error message to appear
        _wait.Until(driver => driver.FindElement(_errorMessageLocator));
 
        return this; // Return current page object since we stay on login page
    }
}

This pattern makes your test flow much more natural and self-documenting. The return type tells you exactly what page you should be on after the action completes, eliminating the guesswork from our previous implementation.

Validation Methods for Robust Testing

Another powerful pattern is adding validation methods to your Page Objects. These methods encapsulate common assertions and replace the direct element checks we used in the previous lesson, making your tests more readable and reducing duplication.

public class LoginPage
{
    // ... existing code ...
 
    // Validation methods make tests more expressive and reliable
    public bool IsErrorMessageDisplayed()
    {
        try
        {
            return ErrorMessage.Displayed;
        }
        catch (NoSuchElementException)
        {
            return false;
        }
    }
 
    public string GetErrorMessageText()
    {
        return ErrorMessage.Text;
    }
 
    public bool IsOnLoginPage()
    {
        // Check for multiple indicators to ensure we're on the right page
        try
        {
            return _driver.Url.Contains("saucedemo.com")
                && UsernameInput.Displayed
                && PasswordInput.Displayed
                && LoginButton.Displayed;
        }
        catch (NoSuchElementException)
        {
            return false;
        }
    }
}

Supporting Page Objects

Our refactored solution also includes a ProductsPage class to handle the inventory page functionality:

using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
 
namespace PageObjects
{
    public class ProductsPage
    {
        private readonly IWebDriver _driver;
        private readonly WebDriverWait _wait;
 
        public ProductsPage(IWebDriver driver)
        {
            _driver = driver;
            _wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
        }
 
        // Locators for products page elements
        private readonly By _inventoryContainerLocator = By.Id("inventory_container");
        private readonly By _productItemsLocator = By.CssSelector("[data-test='inventory-item']");
 
        // Properties for page elements with explicit waits
        public IWebElement InventoryContainer => _wait.Until(d => d.FindElement(_inventoryContainerLocator));
        public IList<IWebElement> ProductItems => _driver.FindElements(_productItemsLocator);
 
        // Validation methods
        public bool IsOnProductsPage()
        {
            try
            {
                return _driver.Url.Contains("inventory.html")
                    && InventoryContainer.Displayed
                    && ProductItems.Count > 0;
            }
            catch (NoSuchElementException)
            {
                return false;
            }
        }
 
        public int GetProductCount()
        {
            return ProductItems.Count;
        }
    }
}

Refactoring the Test to Use POM

Now for the payoff. Let's see how dramatically our test method improves when we refactor it from the previous lesson to use our new Page Object Model approach with robust WebDriverWait integration.

The transformation you're about to see is more than just code reorganization—it's a fundamental shift from the flaky, hard-to-maintain script we had before to a robust, object-oriented, business-focused test automation framework.

The "Before" Code (From Previous Lesson)

Here's what our test looked like in the previous lesson with direct Selenium calls:

namespace RobustInteractions
{
    public class LoginTests
    {
        private IWebDriver _driver;
        private WebDriverWait _wait;
 
        [SetUp]
        public void Setup()
        {
            _driver = new ChromeDriver();
            _wait = new WebDriverWait(_driver, TimeSpan.FromSeconds(10));
        }
 
        [Test]
        public void SuccessfulLoginTest()
        {
            _driver.Navigate().GoToUrl("https://www.saucedemo.com/");
 
            // Wait for the username input to be visible before interacting
            IWebElement usernameInput = _wait.Until(d => d.FindElement(By.Id("user-name")));
            usernameInput.SendKeys("standard_user");
 
            // Direct Selenium interactions scattered throughout the test
            IWebElement passwordInput = _driver.FindElement(By.Id("password"));
            passwordInput.SendKeys("secret_sauce");
 
            IWebElement loginButton = _driver.FindElement(By.Id("login-button"));
            loginButton.Click();
 
            // Manual wait and assertion
            IWebElement inventoryContainer = _wait.Until(d => d.FindElement(By.Id("inventory_container")));
            Assert.IsTrue(inventoryContainer.Displayed, "Login was not successful, inventory page not found.");
        }
    }
}

The "After" Code (Clean and Maintainable)

The test is now shorter, more readable, and completely decoupled from Selenium's implementation details. It reads like a series of business steps, not a sequence of low-level UI interactions.

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
 
namespace PageObjects
{
    public class LoginTests
    {
        private IWebDriver _driver;
 
        [SetUp]
        public void Setup()
        {
            _driver = new ChromeDriver();
            _driver.Manage().Window.Maximize();
        }
 
        [Test]
        public void SuccessfulLoginTest()
        {
            // Arrange: Create an instance of the LoginPage
            var loginPage = new LoginPage(_driver);
            loginPage.NavigateTo();
 
            // Verify we're on the correct starting page
            Assert.IsTrue(loginPage.IsOnLoginPage(), "Should start on login page");
 
            // Act: Use the factory pattern for navigation
            var productsPage = loginPage.LoginSuccessfully("standard_user", "secret_sauce");
 
            // Assert: Use validation methods from ProductsPage
            Assert.IsTrue(productsPage.IsOnProductsPage(), "Should be on products page after successful login");
            Assert.IsTrue(productsPage.GetProductCount() > 0, "Should display products after login");
        }
 
        [TearDown]
        public void TearDown()
        {
            _driver?.Quit();
        }
    }
}

Enhanced Test Coverage with Multiple Scenarios

The Page Object Model makes it easy to add comprehensive test coverage. Here are additional test scenarios that leverage our robust page objects:

[Test]
public void UnsuccessfulLoginTest()
{
    // Arrange
    var loginPage = new LoginPage(_driver);
    loginPage.NavigateTo();
 
    // Act: Stay on login page for failed attempt
    loginPage = loginPage.LoginUnsuccessfully("invalid_user", "wrong_password");
 
    // Assert: Verify error handling
    Assert.IsTrue(loginPage.IsErrorMessageDisplayed(), "Should show error message for invalid login");
    Assert.IsTrue(loginPage.GetErrorMessageText().Contains("Username and password do not match"),
        "Error message should be descriptive");
    Assert.IsTrue(loginPage.IsOnLoginPage(), "Should remain on login page after failed attempt");
}
 
[Test]
public void LoginWithEmptyCredentialsTest()
{
    // Arrange
    var loginPage = new LoginPage(_driver);
    loginPage.NavigateTo();
 
    // Act: Attempt login with empty credentials
    loginPage = loginPage.LoginUnsuccessfully("", "");
 
    // Assert: Verify appropriate error message
    Assert.IsTrue(loginPage.IsErrorMessageDisplayed(), "Should show error message for empty credentials");
    Assert.IsTrue(loginPage.GetErrorMessageText().Contains("Username is required"),
        "Should show username required error");
}
 
[Test]
public void LoginWithLockedUserTest()
{
    // Arrange
    var loginPage = new LoginPage(_driver);
    loginPage.NavigateTo();
 
    // Act: Attempt login with locked user
    loginPage = loginPage.LoginUnsuccessfully("locked_out_user", "secret_sauce");
 
    // Assert: Verify locked user error message
    Assert.IsTrue(loginPage.IsErrorMessageDisplayed(), "Should show error message for locked user");
    Assert.IsTrue(loginPage.GetErrorMessageText().Contains("locked out"),
        "Should show locked out error message");
}

The benefits are immediate and substantial. If the login button's ID changes tomorrow, we only have to make a one-line change in LoginPage.cs. All of our tests that use the LoginSuccessfully() or LoginUnsuccessfully() methods will continue to work without any modification. Compare this to our previous approach where we would need to update every single test method that interacted with the login button.

Key Improvements Over Previous Implementation

Let's summarize the dramatic improvements we've achieved by refactoring our previous lesson's script using the Page Object Model:

Structural Transformation

  • Before: Single test file with direct Selenium calls scattered throughout
  • After: Separate Page Object classes (LoginPage.cs, ProductsPage.cs) and clean test class
  • Benefit: Clear separation of concerns and improved organization

WebDriverWait Integration

  • Before: Wait declarations in test methods, inconsistent wait usage
  • After: Wait initialization moved into Page Object constructors, consistent explicit waits
  • Benefit: Eliminates race conditions and reduces test flakiness

Maintainability Enhancement

  • Before: Locators scattered throughout test methods
  • After: Centralized locators as private fields in page classes
  • Benefit: Single point of maintenance for UI changes

Test Readability

  • Before: Tests read like technical procedures with Selenium method calls
  • After: Tests read like user stories with business-focused method names
  • Benefit: Tests serve as living documentation of application behavior

Error Handling and Robustness

  • Before: Basic assertion with generic error messages
  • After: Comprehensive validation methods with specific error scenarios
  • Benefit: Better test debugging and more thorough validation coverage

This refactoring transforms your test automation from a collection of scripts into a professional, maintainable framework that can scale with your application and team. It addresses critical architectural issues—like duplication, scattered locators, and brittle test logic—but it's only the beginning. In the upcoming lessons, we'll build on this foundation to introduce shared base classes, centralized driver management, and advanced patterns that make your framework even cleaner, faster, and more resilient to change.

Common Page Object Pitfalls and How to Avoid Them

Even with the best intentions, it's easy to fall into anti-patterns when implementing Page Objects. Let's examine the most common mistakes and learn how to avoid them.

Pitfall 1: Exposing WebDriver to Tests

Sometimes test automation engineers create a "shortcut" by exposing the WebDriver instance through a public property on their Page Object. This breaks encapsulation and defeats the purpose of the pattern.

// DON'T DO THIS - Breaks encapsulation
public class LoginPage
{
    public IWebDriver Driver { get; private set; } // This is problematic!

    // Now tests can access Driver directly, bypassing our abstraction
}
 
// This leads to tests like this:
[Test]
public void BadTest()
{
    var loginPage = new LoginPage(_driver);
    // Test directly uses WebDriver - defeats the purpose of POM!
    loginPage.Driver.FindElement(By.Id("user-name")).SendKeys("test");
}

Keep your WebDriver field private and trust your Page Object methods to handle all browser interactions.

Pitfall 2: Creating God Objects

Sometimes test automation engineers try to create one massive Page Object that handles multiple pages or complex workflows. This violates the Single Responsibility Principle and makes maintenance difficult.

// DON'T DO THIS - Too many responsibilities
public class MegaPage
{
    // Handles login, product browsing, cart management, and checkout
    public void Login(string username, string password) { }
    public void AddProductToCart(string productName) { }
    public void UpdateCartQuantity(int quantity) { }
    public void CompleteCheckout(string address) { }
    // ... 50 more methods
}

Instead, create focused Page Objects that correspond to distinct pages or functional areas of your application. If a Page Object has more than 10-15 methods, consider splitting it.

Pitfall 3: Including Assertions in Page Objects

While validation methods are useful, Page Objects should not contain test assertions (Assert statements). Page Objects describe what can be done on a page, while tests decide what should be verified.

// DON'T DO THIS - Assertions belong in tests, not Page Objects
public void Login(string username, string password)
{
    UsernameInput.SendKeys(username);
    PasswordInput.SendKeys(password);
    LoginButton.Click();
 
    // This assertion doesn't belong here!
    Assert.IsTrue(_driver.FindElement(By.Id("inventory_container")).Displayed);
}
 
// DO THIS INSTEAD - Return information, let tests make assertions
public bool IsLoginSuccessful()
{
    try
    {
        return _driver.FindElement(By.Id("inventory_container")).Displayed;
    }
    catch (NoSuchElementException)
    {
        return false;
    }
}

Hands-On Practice: Extending Your Page Objects

Now it's time to apply the POM pattern yourself. In the previous lesson's exercise, you may have worked with adding items to the cart. Since we've already created the ProductsPage class as part of our refactoring, let's extend it with additional functionality to demonstrate the power and flexibility of the Page Object Model.

Your Task: Enhance the ProductsPage Object

  1. Review the existing ProductsPage.cs class we created. Notice how it already includes basic validation methods like IsOnProductsPage() and GetProductCount().
  2. Add additional locators to the ProductsPage class for cart functionality:
    • Add to cart buttons for specific products (e.g., "Sauce Labs Backpack")
    • Shopping cart badge that shows item count
    • Shopping cart link for navigation
  3. Create new service methods on your ProductsPage class:
    • AddItemToCart(string itemName) - Adds a specific item to the cart
    • GetShoppingCartBadgeCount() - Returns the number displayed on the cart badge
    • NavigateToCart() - Clicks the cart and returns a new CartPage object (Factory pattern)
  4. Add a new file ProductsTests.cs for the ProductsPage tests.
  5. Create a new test method, AddItemToCartTest, in the ProductsTests.cs file. This test should use both your LoginPage and enhanced ProductsPage to perform the full workflow: Login → Add Item → Assert Cart Count.
  6. Bonus: Create a CartPage class to handle cart-specific functionality, following the same patterns we've established.

This exercise will solidify your understanding of how different Page Objects work together to model a complete user journey, and demonstrate how easy it is to extend functionality when using the Page Object Model pattern.

Here's a starting point for enhancing the ProductsPage with cart functionality:

// Additional locators for cart functionality
private readonly By _backpackAddToCartLocator = By.Id("add-to-cart-sauce-labs-backpack");
private readonly By _shoppingCartBadgeLocator = By.CssSelector("[data-test='shopping-cart-badge']");
private readonly By _shoppingCartLinkLocator = By.CssSelector("[data-test='shopping-cart-link']");
 
// Enhanced service methods
public void AddBackpackToCart()
{
    var addButton = _wait.Until(d => d.FindElement(_backpackAddToCartLocator));
    addButton.Click();
}
 
public string GetShoppingCartBadgeCount()
{
    try
    {
        return _driver.FindElement(_shoppingCartBadgeLocator).Text;
    }
    catch (NoSuchElementException)
    {
        return "0"; // No badge means empty cart
    }
}
 
public bool IsShoppingCartBadgeVisible()
{
    try
    {
        return _driver.FindElement(_shoppingCartBadgeLocator).Displayed;
    }
    catch (NoSuchElementException)
    {
        return false;
    }
}

Reflection: The Bigger Picture

Before we move on to building a complete framework foundation in the next lesson, take a moment to reflect on what we've accomplished. You've learned more than just a coding pattern—you've learned a way of thinking about test automation that will serve you throughout your career.

The Page Object Model represents a fundamental shift from "script" thinking to "architecture" thinking. Instead of writing sequences of actions, you're now designing maintainable, reusable components that model your application's behavior. This architectural mindset is what separates experienced automation engineers from beginners.

Consider how this pattern will scale as your test suite grows. With 100 tests across 20 pages, the difference between a well-structured POM framework and scattered scripts becomes dramatic. Changes that would require touching dozens of files now require single-line updates. Tests that were cryptic technical procedures now read like clear business scenarios.

In our next lesson, we'll build upon this foundation to create a complete test automation framework. We'll explore how Page Objects fit into a larger architectural picture that includes configuration management, test data handling, reporting, and parallel execution. The organizational principles you've learned here will scale up to support enterprise-level test automation.

Key Takeaways

  • The Page Object Model (POM) is an essential design pattern that separates test logic from UI interaction logic, creating maintainable and scalable test automation.
  • Page Objects contain locators and service methods; Tests contain business logic and assertions. This separation of concerns is crucial for long-term maintainability.
  • POM makes your test suite dramatically more readable, reusable, and maintainable. A single UI change requires only a single update in one Page Object, rather than scattered changes across multiple test files.
  • For modern applications, extend the pattern to Component Objects to model reusable UI widgets like headers or search bars, mirroring your application's architectural patterns.
  • A well-structured Page Object uses private locators, public element properties with lazy loading, and public service methods that represent user workflows rather than technical steps.
  • Advanced patterns like the Factory pattern for navigation and validation methods for assertions make Page Objects even more powerful and expressive.
  • Common pitfalls include exposing WebDriver to tests, creating overly complex "God Objects", and including test assertions within Page Object methods.
  • The Page Object Model represents a shift from procedural scripting to object-oriented, architecture-focused test automation design.

Deepen Your POM Knowledge

What's Next?

You've now architected a clean, maintainable, and object-oriented test. This is a massive leap forward. However, our test setup code is still a bit messy. We are creating a new IWebDriver instance in the [SetUp] method of every test class and passing it into every Page Object we create. This is repetitive and can be improved.

In our next lesson, we will elevate our solution from a collection of Page Objects into a true Framework Foundation. We'll introduce a BasePage for shared logic and a DriverManager to centralize browser management, making our code even cleaner and more scalable.