BaseTest, BasePage and the limits of inheritance
A typical framework has two "base" classes:
- BaseTest — shared setup and teardown: reading configuration, creating and closing the driver or client, preparing data. Test classes extend it.
- BasePage — what every page needs: a reference to the driver, waits, shared helper methods.
A base class is convenient but has a limit. Inheritance models "is a kind of" (is-a), and deep chains such as LoginPage → AuthPage → CommonPage → BasePage quickly become brittle: a change in an ancestor breaks all descendants, and a class inherits methods it does not need.
Composition models "has" (has-a): a page holds a component such as HeaderComponent or Table as a field and delegates work to it. One component is used by different pages without a common ancestor.
class OrdersPage extends BasePage {
private final HeaderComponent header = new HeaderComponent(driver);
private final DataTable table = new DataTable(driver);
}
Practical rules:
- A Java class has only one parent, so do not stretch the "base": keep it thin.
- The test asserts, not the page; the page provides actions and state.
- Locators and internal helpers are
private, actions for tests are public, what descendants need is protected.
- A method that a page returns after an action can return the next page (fluent):
loginPage.login(u).openOrders().
Interfaces, final, static and why a static driver breaks parallel runs
An interface is a contract without state: what an object can do, regardless of the implementation. A class can implement several interfaces but extend only one class. In a framework interfaces fit where implementations are interchangeable: a DriverFactory for local runs and Grid/cloud, a TestDataProvider from a file or from an API, a Reporter. A test depends on the interface and configuration chooses the implementation. A default method in an interface provides a default implementation (Java 8+).
An abstract class fits when you need shared state and some ready code (for example BaseTest).
final has three meanings:
- a variable — it can be assigned only once; for a reference this forbids reassigning but does not freeze the content (a
final List can still grow);
- a method — it cannot be overridden;
- a class — it cannot be extended (
String is final).
Access modifiers: private — the class only; none (package-private) — the package; protected — the package and subclasses; public — everyone. The narrower the access, the more freely the implementation can change.
static belongs to the class, not to an object, so it is shared by all threads. A classic mistake:
public class BaseTest {
protected static WebDriver driver; // one field for the whole JVM
void setUp() { driver = new ChromeDriver(); }
}
In a parallel run each test overwrites another test's driver, closes a foreign browser or clicks on the wrong page; failures are unstable and hard to reproduce. The fix: an instance field (when the runner creates a separate instance of the class for each test, as JUnit 5 does by default) or ThreadLocal (see the parallelism chapter). Static variables with state cause the same problem: the current user, a token, counters. Static stateless utilities (for example date formatting) are safe.