Sustainable Selenium: Building a Framework Foundation

In our last lesson, we took a monumental step forward by organizing our UI logic into Page Objects. Our tests are now clean, readable, and far more maintainable. But if we look closely at our test classes themselves, a new problem emerges: repetitive setup code.

What happens when we need to add ten more test classes for different features? We'll be forced to copy and paste the same WebDriver initialization and teardown logic into every single file. This is a maintenance trap waiting to spring.

This lesson is about escaping that trap. We will elevate our solution from a collection of well-organized objects into a true, reusable framework foundation. We'll abstract away the boilerplate, centralize control, and build a scalable structure that will support our test suite as it grows. 🏗️

Tommy and Gina are building a robot framework foundation

The Problem – Repetitive Boilerplate

Let's perform another code review, this time focusing on the test class structure from our 03-page-objects project. While the test methods are clean, the surrounding class structure is not.

public class LoginTests
{
    private IWebDriver _driver; // Problem #1: Every test class needs this

    [SetUp]
    public void Setup()
    {
        _driver = new ChromeDriver(); // Problem #2: Repeated initialization
        _driver.Manage().Window.Maximize();
    }

    [Test]
    public void SuccessfulLoginTest()
    {
        // The test logic is clean...
        var loginPage = new LoginPage(_driver); // Problem #3: Manually passing the driver
        // ...
    }

    [TearDown]
    public void TearDown()
    {
        _driver?.Quit(); // Problem #4: Repeated teardown logic
    }
}

The Scalability Issues

  • Code Duplication: The [SetUp] and [TearDown] logic must be duplicated in every test class (LoginTests, ProductsTests, CheckoutTests, etc.). This violates the DRY (Don't Repeat Yourself) principle.
  • Inconsistent State: If a new engineer adds a test class but forgets to add _driver.Manage().Window.Maximize(), their tests will run in a different state, leading to inconsistent results and debugging headaches.
  • Difficult Maintenance: Imagine you need to add a Chrome Option to run all tests in headless mode for a CI/CD pipeline. With this structure, you would have to find and edit every single test class file. This is inefficient and error-prone.

A professional framework solves this by centralizing all common setup and teardown logic in one place.

Solution Part 1 – The Base Test

To solve the problem of repetitive test setup, we'll use one of the core principles of object-oriented programming: inheritance. We will create a single BaseTest class that contains all the common setup and teardown logic. Our actual test classes will then inherit from this base class, automatically gaining all its functionality.

Implementing the BaseTest Class

In our 04-framework-structure project, we create a new file, BaseTest.cs. This class is not for writing tests; it's a blueprint for all of our test classes.

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using NUnit.Framework;

namespace FrameworkStructure
{
    public class BaseTest
    {
        // Change to protected so child classes can access it
        protected IWebDriver _driver;

        [SetUp]
        public void Setup()
        {
            // All setup logic is now centralized here
            _driver = new ChromeDriver();
            _driver.Manage().Window.Maximize();
        }

        [TearDown]
        public void TearDown()
        {
            // All teardown logic is centralized here
            _driver?.Quit();
        }
    }
}

Refactoring Our Test Class

Now, we can dramatically simplify our LoginTests class. By adding : BaseTest, we tell C# that LoginTests inherits all the members (fields and methods) of BaseTest.

// Notice the ": BaseTest" inheritance
public class LoginTests : BaseTest
{
    // The _driver field, [SetUp], and [TearDown] methods are gone!
    // They are automatically inherited from BaseTest.

    [Test]
    public void SuccessfulLoginTest()
    {
        // We can still access _driver because it's "protected" in the base class
        var loginPage = new LoginPage(_driver);
        loginPage.NavigateTo();

        var productsPage = loginPage.LoginSuccessfully("standard_user", "secret_sauce");

        Assert.IsTrue(productsPage.IsOnProductsPage(), "Should be on products page after successful login");
    }
}

With this change, our test classes are now lean and focused purely on testing. Any changes to browser setup are now made in one single place: BaseTest.cs.

Solution Part 2 – The Base Page

We can apply the exact same principle of inheritance to our Page Objects. If we look at LoginPage.cs and ProductsPage.cs, we can see they both have a private _driver field and a constructor that initializes it. This is another form of duplication.

Implementing the BasePage Class

We'll create a BasePage.cs that will serve as the foundation for all of our Page Objects. It will hold the shared WebDriver instance.

using OpenQA.Selenium;

namespace FrameworkStructure
{
    public class BasePage
    {
        // Use "protected" to allow child classes to access the driver and wait objects
        protected readonly IWebDriver driver;
        protected readonly WebDriverWait wait;

        // The constructor that all child Page Objects will call
        public BasePage(IWebDriver driver)
        {
            this.driver = driver;
            wait = new WebDriverWait(this.driver, TimeSpan.FromSeconds(10));
        }
    }
}

Refactoring Our Page Object

Now we can simplify our LoginPage. It will inherit from BasePage and use the base keyword to pass the driver instance up to the parent constructor.

using OpenQA.Selenium;

// Notice the ": BasePage" inheritance
public class LoginPage : BasePage
{
    // Private fields are gone! It's inherited from BasePage.

    // The constructor now calls the base constructor to pass the driver up
    public LoginPage(IWebDriver driver) : base(driver)
    {
    }

    // Locators and methods remain the same...
    private readonly By _usernameLocator = By.Id("user-name");
    public IWebElement UsernameInput => _driver.FindElement(_usernameLocator);
    // ...
}

This makes our Page Objects simpler and provides a central place (BasePage.cs) where we can add common helper methods that all pages might need, such as a method to get the page title or wait for an element to be visible – a concept we will explore in the next lesson!

The New Framework Architecture

By implementing these two base classes, we have created a clean, scalable, and professional framework foundation. The flow of control is now elegant and centralized.

classDiagram BaseTest <|-- LoginTests BaseTest <|-- ProductsTests BasePage <|-- LoginPage BasePage <|-- ProductsPage LoginTests --> LoginPage : Instantiates ProductsTests --> LoginPage : Instantiates LoginPage --> ProductsPage : Instantiates
Our new framework structure uses inheritance to reduce duplication and centralize logic.

Hands-On Practice: Applying the Foundation

Now it's your turn to apply these powerful abstraction techniques to solidify your understanding.

Your Task: Refactor the Products Test

  1. Navigate to the 04-framework-structure folder in the course repository. Examine the new BaseTest.cs and BasePage.cs files.
  2. Take the ProductsPage.cs class we created in the last lesson and refactor it to inherit from BasePage.cs. This will involve removing its local driver and wait fields and updating its constructor to call base(driver).
  3. Create a new test class file, ProductsTests.cs. Make sure this class inherits from BaseTest.
  4. Move the AddItemToCartTest method you wrote previously into this new ProductsTests class. Ensure it uses the refactored ProductsPage and runs successfully.

When you're done, you will have two clean test classes (LoginTests and ProductsTests) that both inherit from BaseTest, and two clean Page Objects (LoginPage and ProductsPage) that both inherit from BasePage. This is the blueprint for a scalable framework.

Key Takeaways

  • A true framework moves beyond design patterns (like POM) and implements architectural solutions to reduce code duplication and centralize control.
  • A BaseTest class uses inheritance to share test setup ([SetUp]) and teardown ([TearDown]) logic across all test classes.
  • A BasePage class uses inheritance to share common components, like the IWebDriver instance, across all Page Objects.
  • Using protected access modifiers allows child classes to access members of the base class, enabling this pattern.
  • This foundational structure makes your test suite significantly more scalable and easier to maintain.

Deepen Your C# Knowledge

What's Next?

You have successfully built a clean, scalable framework foundation. We've eliminated boilerplate code and centralized our core logic. Our architecture is now ready to handle increasing complexity. Now, it's time to put it to the test.

In our next lesson, we will use this robust foundation to tackle more complex, real-world testing problems. We'll dive into Advanced Interactions & Problem Solving, learning how to handle tricky UI elements, deal with dynamic content, and debug challenging exceptions like the dreaded StaleElementReferenceException.