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. 🏗️
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.
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
- Navigate to the
04-framework-structurefolder in the course repository. Examine the newBaseTest.csandBasePage.csfiles. - Take the
ProductsPage.csclass we created in the last lesson and refactor it to inherit fromBasePage.cs. This will involve removing its local driver and wait fields and updating its constructor to callbase(driver). - Create a new test class file,
ProductsTests.cs. Make sure this class inherits fromBaseTest. - Move the
AddItemToCartTestmethod you wrote previously into this newProductsTestsclass. Ensure it uses the refactoredProductsPageand 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
BaseTestclass uses inheritance to share test setup ([SetUp]) and teardown ([TearDown]) logic across all test classes. - A
BasePageclass uses inheritance to share common components, like theIWebDriverinstance, across all Page Objects. - Using
protectedaccess 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
- Microsoft Docs: Inheritance in C# The official documentation on the core concept of inheritance that powers our framework foundation.
- Microsoft Docs: base keyword A detailed explanation of the `base` keyword, used to access members of the base class from within a derived class.
- Microsoft Docs: protected Access Modifier Understand why the `protected` keyword is the perfect choice for sharing the WebDriver instance between base and derived classes.