QA Interview Hub · Knowledge base · Quizzes

Java basics

OOP in a framework

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.