Classes and Objects

8 min read

Up to now every line you've written has lived inside public class Whatever { public static void main(...) { ... } }. The class was just a wrapper to satisfy Java's "everything in a class" rule. This chapter takes the wrapper seriously: classes are how Java organises data and behaviour together, and they are the foundation of every page object, every test data model, and every framework abstraction you'll write. If you've ever wondered why the same Selenium tutorial mentions LoginPage and extends BasePage, this chapter is the answer.

Class = blueprint, object = instance

A class describes the shape of a thing. An object is one specific thing that follows that shape. The cookie-cutter analogy is overused but it's exactly right: the cutter (class) defines what cookies look like; each cookie (object) is a real piece of dough with its own decorations. The cutter is information — you cannot eat it. The cookies are instances — they live, they're different from each other, you can have many of them.

In QA terms:

  • LoginPage (the class) describes what every login page object knows and does. It has fields like emailInput and submitButton, and methods like login(...).
  • The loginPage variable in your test (the object) is one specific login page tied to the current WebDriver, with its own state.
  • Two different tests can have two different LoginPage objects, both built from the same class, both behaving the same way.

Defining a class

A simple test-data class:

public class TestUser {
    String name;
    String email;
    String role;
}

Three fields (also called instance variables): each TestUser object will have its own copy of name, email, and role. We've left them with no access modifier on purpose for now; lesson 3 covers private and the encapsulation pattern.

The class lives in a file named TestUser.java. The rule from chapter 1.3 still applies: a public class must live in a file with the matching name. If you have multiple classes in one project, each public class lives in its own .java file.

Creating an object — the new keyword

new TestUser() allocates a new instance in memory and returns a reference to it. You store the reference in a variable typed by the class:

public class UserPlayground {
    public static void main(String[] args) {
        TestUser admin = new TestUser();
        admin.name = "Alice";
        admin.email = "alice@test.com";
        admin.role = "admin";
 
        System.out.println(admin.name + " (" + admin.role + ") — " + admin.email);
    }
}

Output:

Alice (admin) — alice@test.com

Read each piece:

  • TestUser admin — declare a variable of type TestUser. Currently it points at nothing (null).
  • new TestUser() — build a fresh instance. The parentheses are calling the constructor (lesson 2).
  • admin.name = "Alice" — dot syntax to access a field on the object. Same shape as JavaScript and Python.

The variable admin doesn't contain the user; it contains a reference to where the user lives in the heap. This matters for everything in chapter 3.2 (pass-by-value-of-references) — when you pass admin into a method, both names point at the same underlying object.

Many objects, one class

The whole reason classes exist is that you can stamp out many independent objects from the same blueprint:

public class UserDirectory {
    public static void main(String[] args) {
        TestUser admin = new TestUser();
        admin.name = "Alice";
        admin.role = "admin";
 
        TestUser standard = new TestUser();
        standard.name = "Bob";
        standard.role = "member";
 
        TestUser guest = new TestUser();
        guest.name = "Carol";
        guest.role = "guest";
 
        TestUser[] users = {admin, standard, guest};
        for (TestUser u : users) {
            System.out.println(u.name + " is a " + u.role);
        }
    }
}

Output:

Alice is a admin
Bob is a member
Carol is a guest

Three objects, each with its own name and role. Setting admin.role doesn't touch standard.role — they are separate instances. That independence is the property classes exist to provide.

Adding behaviour — methods on a class

Classes can hold methods, not just fields. Methods declared without static are instance methods — they belong to a specific object and operate on that object's fields:

public class TestCase {
    String name;
    String priority;
    boolean passed;
 
    public void printSummary() {
        String label = passed ? "✅ PASS" : "❌ FAIL";
        System.out.println(label + " [" + priority + "] " + name);
    }
}

Calling it:

public class CaseRunner {
    public static void main(String[] args) {
        TestCase login = new TestCase();
        login.name = "Login as admin";
        login.priority = "P0";
        login.passed = true;
 
        TestCase search = new TestCase();
        search.name = "Search by SKU";
        search.priority = "P2";
        search.passed = false;
 
        login.printSummary();
        search.printSummary();
    }
}

Output:

✅ PASS [P0] Login as admin
❌ FAIL [P2] Search by SKU

Notice what changed compared to chapter 3: there's no static on printSummary, and we don't call it as TestCase.printSummary(login) — we call it as login.printSummary(). The method runs on a specific object, and inside the method name, priority, and passed refer to that object's fields. This is the dot syntax you've already seen in driver.findElement(...) and cy.get(...) — the method belongs to the object on the left of the dot.

One class, many instances

TestUser (class)
  • – name = 'Alice'
  • – role = 'admin'
  • – email = 'alice@test.com'
  • – name = 'Bob'
  • – role = 'member'
  • – email = 'bob@test.com'
  • name = 'Carol' –
  • role = 'guest' –
  • email = 'carol@test.com' –

The class in the centre describes the shape (three fields). Each instance fills the shape with its own values. That mental picture — one class, many instances — is the whole foundation of OOP.

Why this matters for QA

The Selenium Page Object Model, the most common pattern in UI test code, is just classes and objects:

  • LoginPage is a class. It declares fields for the email input, password input, submit button, plus methods like login(email, password) and getErrorMessage().
  • A test creates an object: LoginPage loginPage = new LoginPage(driver);
  • The test calls behaviour through the object: loginPage.login("alice@x.com", "secret");

Every test framework you'll use does this. TestNG @Test methods live on a test class. Rest Assured response objects expose data through methods. Reading and writing classes fluently is the difference between scripting tests and engineering a test suite.

⚠️ Common mistakes

  • Forgetting new. TestUser admin; declares a variable of type TestUser but doesn't create an object — admin is null. The first time you call admin.name = "...", you get a NullPointerException. The fix: TestUser admin = new TestUser();.
  • Confusing the class name with an instance. TestUser.name = "Alice" is wrong — name belongs to instances, not to the class. You can only use Class.something for static fields and methods. Field access goes through an instance: admin.name.
  • Multiple public classes in one file. public class TestUser and public class LoginPage cannot share a .java file. One public class per file, and the file must be named after it. Non-public (package-private) classes can share a file but rarely should — keep one class per file as a habit.

🎯 Practice task

Build and use a small data class. 25-30 minutes.

  1. Create two files in the same folder: TestCase.java and CaseRunner.java.
  2. In TestCase.java, declare public class TestCase with fields String name, String priority, long durationMs, boolean passed.
  3. Add an instance method public void printSummary() that prints ✅ PASS [P0] Login as admin (1450ms) style — pull the values from the object's own fields.
  4. Add a second instance method public boolean isCritical() that returns true if priority.equals("P0").
  5. In CaseRunner.java, declare public class CaseRunner with a main method.
  6. Inside main, create at least three TestCase objects with new TestCase() and set their fields. Mix priorities and pass/fail values.
  7. Put them in a TestCase[] array and loop with for-each. For each, call printSummary(), then if isCritical() returns true and passed is false, also print ⚠️ CRITICAL FAILURE.
  8. Compile both files together: javac TestCase.java CaseRunner.java. Then java CaseRunner. Confirm the output matches your inputs.
  9. Stretch: add a static int totalCases = 0; field on TestCase. Increment it inside a printSummary call (or extract to a method) and print it at the end of the run. Static fields belong to the class, not to instances — every object shares the same counter. Reading the difference is a fast way to see why most fields are not static.

You've now built the smallest interesting unit of OO code: a class with fields, a class with methods, and instances created with new. Lesson 2 fixes the awkward "set every field by hand" pattern with constructors.

// tip to track lessons you complete and pick up where you left off across devices.