Simplified Guide to Java's Object-Oriented Principles for Building Frameworks

Table of contents

In this guide we will discuss some core Java concepts essential for framework development, such as inheritance, TestNG annotations, constructors, and the use of keywords like this
and super
. Let’s get started!
Introduction to Core Java Concepts for Framework Development
Before diving into framework development (like building automation frameworks with Selenium), it’s important to grasp some core Java concepts. These concepts form the foundation for writing clean, reusable, and efficient code. In this guide, we’ll focus on:
Inheritance: How one class can inherit properties and methods from another.
TestNG Annotations: Using annotations like
@Test
,@BeforeMethod
, and@AfterMethod
to organize test execution.Constructors: Understanding how to initialize objects with or without parameters.
The
this
andsuper
Keywords: Differentiating between instance and class variables and invoking parent class constructors.Single Responsibility Principle: Organizing reusable code in separate classes.
Let’s get started with the first concept: Inheritance.
What is Inheritance in Java?
Inheritance is a powerful OOP feature in Java that allows one class (called the child class) to acquire the properties (fields) and behaviors (methods) of another class (called the parent class). This promotes code reuse and establishes a parent-child relationship between classes.
Example: Parent and Child Classes
Imagine you have a class called PS
(Parent Superclass) that contains some methods and variables. You want another class, say PS1
, to access all the methods and variables defined in PS
. To do this, you make PS1
a child class of PS
using the extends keyword.
Here’s how it works:
// Parent class
public class PS {
public void doThis() {
System.out.println("I am here");
}
}
// Child class
public class PS1 extends PS {
// Inherits doThis() from PS
}
In this example:
PS
is the parent class with a methoddoThis()
.PS1
is the child class that extends PS, meaning it inherits thedoThis()
method.By using the extends keyword,
PS1
can directly calldoThis()
without creating an object ofPS
.
Why Use Inheritance?
Without inheritance, to access a method from another class, you’d need to create an object of that class. For example:
PS ps = new PS();
ps.doThis();
With inheritance, you don’t need to create an object of the parent class. The child class automatically has access to the parent’s methods and variables, making the code cleaner and more efficient.
Integrating TestNG with Inheritance
In the context of testing frameworks like Selenium, we often use TestNG, which is a testing framework that simplifies test execution. Instead of using the traditional public static void main
method to run a program, TestNG allows us to use annotations like @Test
to mark methods as test cases.
Let’s see how TestNG works with inheritance:
Example: Using @Test
in the Child Class
Suppose you want to run the doThis()
method from the parent class PS
in the child class PS1
. You can add a TestNG @Test
annotation to a method in PS1
to execute it as a test case.
import org.testng.annotations.Test;
public class PS1 extends PS {
@Test
public void testMethod() {
doThis(); // Calls the inherited method from PS
}
}
When you run this test, TestNG executes the testMethod()
, which calls doThis()
from the parent class PS
. The output will be:
I am here
Adding @BeforeMethod
and @AfterMethod
TestNG provides annotations like @BeforeMethod
and @AfterMethod
to run setup and teardown code before and after each test method. These are particularly useful in frameworks to handle repetitive tasks, such as opening a browser in Selenium.
Let’s add a @BeforeMethod
to the parent class PS
:
import org.testng.annotations.BeforeMethod;
public class PS {
public void doThis() {
System.out.println("I am here");
}
@BeforeMethod
public void beforeRun() {
System.out.println("Run me first");
}
}
Now, when you run the test in PS1
, TestNG ensures that beforeRun()
from the parent class executes before the @Test
method in the child class. The output will be:
Run me first
I am here
Similarly, you can add an @AfterMethod
in the parent class:
import org.testng.annotations.AfterMethod;
public class PS {
public void doThis() {
System.out.println("I am here");
}
@BeforeMethod
public void beforeRun() {
System.out.println("Run me first");
}
@AfterMethod
public void afterRun() {
System.out.println("Run me last");
}
}
When you run the test in PS1
, the execution order will be:
beforeRun()
(from parent class)testMethod()
(callsdoThis()
from parent)afterRun()
(from parent class)
The output will be:
Run me first
I am here
Run me last
Why This Matters for Frameworks
In a testing framework like Selenium, you can place common setup code (e.g., opening a browser, maximizing the window) in the @BeforeMethod
of the parent class and cleanup code (e.g., closing the browser) in the @AfterMethod
. The child class can then focus solely on the test logic, making the code cleaner and more maintainable.
This is where inheritance shines: it allows you to move boilerplate code to the parent class, so the child class only contains the actual test logic.
The Single Responsibility Principle and Reusable Utility Classes
As your codebase grows, you’ll want to organize reusable code into separate classes to follow the Single Responsibility Principle. This principle states that each class should have one specific responsibility, making the code easier to maintain and reuse.
Example: Creating a Utility Class
Let’s say you have a variable int A = 3
in your test class (PS1
), and you want to perform operations like incrementing or decrementing it. Instead of writing these operations in the test class, you can create a separate utility class called PS2
to handle these operations.
Here’s how you can create the utility class:
public class PS2 {
int A; // Class variable
public PS2(int A) { // Parameterized constructor
this.A = A; // Assign instance variable to class variable
}
public int increment() {
return A + 1;
}
public int decrement() {
return A - 1;
}
}
Understanding Constructors
A constructor is a special method in a class that’s called when you create an object of that class. It has the same name as the class and no return type. In the example above, PS2
has a parameterized constructor that takes an integer parameter A
.
When you create an object of PS2
in the test class, you pass the value of A:
public class PS1 extends PS {
@Test
public void testMethod() {
int A = 3;
PS2 ps2 = new PS2(A); // Pass A to the constructor
System.out.println(ps2.increment()); // Should print 4
System.out.println(ps2.decrement()); // Should print 2
}
}
The this
Keyword
In the PS2
constructor, you see this.A = A
. Here, this
refers to the current class’s variable (A in PS2
). The parameter A
is an instance variable (local to the constructor), while this.A
is the class variable (global to the class). By assigning this.A = A
, you store the value passed to the constructor in the class variable, making it accessible to all methods in PS2
.
When you run the test, the output will be:
4
2
This happens because:
The constructor sets
this.A = 3
.increment()
returnsA + 1 = 4
.decrement()
returnsA - 1 = 2
.
Why Use a Parameterized Constructor?
Instead of passing the value A to every method (e.g., increment(int A)
), you pass it once when creating the object. This reduces redundancy and makes the code cleaner. The constructor ensures the class is initialized with the necessary data, and all methods can use that data.
The super Keyword and Advanced Inheritance
Now, let’s explore a more complex scenario involving the super
keyword, which is used to call the parent class’s constructor when working with inheritance.
Scenario: Adding a Multiplication Utility Class
Suppose your senior engineer asks you to create a separate utility class, PS3
, for multiplication operations (e.g., multiplying a number by 2 or 3). You create the PS3
class like this:
public class PS3 {
int A;
public PS3(int A) {
this.A = A;
}
public int multiplyByTwo() {
return A * 2;
}
public int multiplyByThree() {
return A * 3;
}
}
To use this in PS1
, you’d typically create an object of PS3
:
public class PS1 extends PS {
@Test
public void testMethod() {
int A = 3;
PS2 ps2 = new PS2(A);
PS3 ps3 = new PS3(A); // Create a separate object
System.out.println(ps2.increment()); // Prints 4
System.out.println(ps3.multiplyByThree()); // Prints 9
}
}
The output will be:
4
9
The Problem: Multiple Objects
Your senior engineer reviews your code and suggests avoiding multiple object creations (PS2
and PS3
). They want you to access the multiplyByThree()
method using the PS2
object. Is this possible? Yes, with inheritance!
Solution: Inherit PS3
in PS2
You can make PS2
extend PS3
, so PS2
inherits the multiplication methods from PS3
:
public class PS2 extends PS3 {
int A;
public PS2(int A) {
super(A); // Call the parent class constructor
this.A = A;
}
public int increment() {
return A + 1;
}
public int decrement() {
return A - 1;
}
}
Now, in PS1
, you only need to create a PS2
object:
public class PS1 extends PS {
@Test
public void testMethod() {
int A = 3;
PS2 ps2 = new PS2(A); // One object
System.out.println(ps2.increment()); // Prints 4
System.out.println(ps2.multiplyByThree()); // Prints 9
}
}
Understanding the super
Keyword
The super(A)
line in the PS2
constructor calls the parent class (PS3
) constructor, passing the value of A. This ensures that the A variable in PS3
is initialized with the value passed from PS1
(in this case, 3
).
Key points about super
:
It’s used to invoke the parent class’s constructor.
It must be the first line in the child class’s constructor.
It’s only meaningful when there’s an inheritance relationship (i.e., the class extends another class).
When you run the test, the output is:
4
9
This works because:
The
PS2
constructor callssuper(A)
, which initializes A inPS3
.The
multiplyByThree()
method inPS3
uses the initialized A to compute3 * 3 = 9
.
Why This Matters ?
By using inheritance and the super
keyword, you avoid creating multiple objects, making the code more efficient. This is a practical example of how inheritance can simplify access to methods across classes, which is especially useful in frameworks where you want to minimize redundancy.
Practical Applications in Selenium Frameworks
The concepts we’ve covered - inheritance, TestNG annotations, constructors, and the super keyword are critical when building testing frameworks like Selenium. Here’s how they apply:
Inheritance: Move common setup and teardown code (e.g., opening a browser) to a parent class, so test classes focus only on test logic.
TestNG Annotations: Use
@BeforeMethod
and@AfterMethod
in the parent class to handle repetitive tasks, ensuring they run automatically before and after tests.Constructors: Initialize reusable utility classes with necessary data (e.g., browser configurations) using parameterized constructors.
Single Responsibility Principle: Organize utility methods (e.g., increment, decrement, or browser actions) in separate classes to keep the code modular.
The
super
Keyword: When utility classes inherit from each other, usesuper
to ensure parent class initialization, reducing the need for multiple objects.
Key Takeaways
Here’s a summary of what we’ve learned:
Inheritance allows a child class to inherit methods and variables from a parent class using the extends keyword, reducing the need for object creation.
TestNG Annotations like
@Test
,@BeforeMethod
, and@AfterMethod
help organize test execution, and inherited methods (including annotations) are available in the child class.Constructors (especially parameterized ones) initialize objects with data, and the
this
keyword differentiates between instance and class variables.The
super
Keyword calls the parent class’s constructor, ensuring proper initialization in an inheritance hierarchy.Single Responsibility Principle encourages moving reusable code to separate utility classes, making the codebase modular and maintainable.
These concepts are foundational for building efficient Selenium frameworks, where you can separate setup, test logic, and utility methods.
Conclusion
Understanding inheritance, TestNG, constructors, and the super
keyword is essential for writing clean and efficient Java code, especially for framework development. These concepts help you organize code logically, reduce redundancy, and make your tests more maintainable. In the context of Selenium, they allow you to create robust automation frameworks by separating concerns and reusing code effectively.
In the next articles, we’ll explore how to apply these concepts to build a Selenium framework from scratch. Check out the complete Selenium Series for more such articles!
Stay tuned, and happy coding!
Subscribe to my newsletter
Read articles from Samiksha Kute directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Samiksha Kute
Samiksha Kute
Passionate Learner!