Mastering Java Automation Testing for UI and Backend: A Practical Guide

// table of contents

Introduction to Java Automation Testing for UI and Backend

Welcome, aspiring automation engineer! This document is designed to be your comprehensive, hands-on guide to mastering Java Automation Testing for both User Interface (UI) and Backend (API) applications. If you’re new to automation or even Java, don’t worry – we’ll start from the ground up, focusing on practical, code-driven examples to make learning engaging and effective.

What is Java Automation Testing?

Java Automation Testing involves using the Java programming language along with various tools and frameworks to automate the process of testing software applications. Instead of manually clicking through a website or sending requests to an API, you write code that performs these actions and verifies the results.

We’ll primarily focus on two key areas:

  • UI Automation: Testing the graphical user interface of web applications, simulating user interactions like clicking buttons, typing text, and verifying elements on a page. For this, we’ll use Selenium WebDriver.
  • Backend (API) Automation: Testing the Application Programming Interfaces (APIs) that drive the application’s logic and data exchange. This involves sending requests to an API and validating its responses. For this, we’ll use Rest Assured.

Why Learn Java Automation Testing?

Learning Java automation testing offers numerous benefits in today’s fast-paced software development world:

  • Efficiency and Speed: Automated tests run much faster and more consistently than manual tests. This allows for quicker feedback cycles in Agile and DevOps environments, accelerating release cycles.
  • Accuracy and Reliability: Computers don’t get tired or make human errors. Automated tests execute the same steps precisely every time, leading to more reliable test results.
  • Wider Test Coverage: You can write a vast number of automated tests to cover various scenarios, edge cases, and regressions that would be impractical to test manually.
  • Early Bug Detection (“Shift-Left”): Automating tests early in the development cycle helps catch bugs when they are easier and cheaper to fix, significantly reducing development costs.
  • Regression Testing: As new features are added, automated tests can quickly confirm that existing functionalities haven’t broken (regressed), providing confidence in each new release.
  • Industry Relevance: Java is a widely used language in enterprise environments, and Java-based automation tools like Selenium and Rest Assured are industry standards. Possessing these skills makes you highly employable.
  • Foundation for CI/CD: Automation testing is a cornerstone of Continuous Integration (CI) and Continuous Delivery (CD) pipelines, enabling automatic testing and deployment of code changes.

A Brief History (Concise)

  • Selenium: Started in 2004, Selenium evolved from a JavaScript test framework to a powerful suite including WebDriver, IDE, and Grid, becoming the de-facto standard for web UI automation. Selenium 4, the latest major version, became fully W3C compliant, bringing improved stability and new features like relative locators.
  • Rest Assured: Born out of the need to simplify REST API testing in Java, Rest Assured was released to provide a domain-specific language (DSL) that makes API test creation and validation more intuitive and readable, mimicking the simplicity of dynamic languages.

Setting Up Your Development Environment

To follow along with this guide, you’ll need to set up a few tools. Don’t worry, we’ll go step-by-step.

Prerequisites:

  1. Java Development Kit (JDK) 11 or higher: Java is the foundation.
  2. An Integrated Development Environment (IDE): IntelliJ IDEA Community Edition is highly recommended for its excellent Java support.
  3. Apache Maven: A build automation tool that will manage our project dependencies.

Let’s get started with the setup:

Step 1: Install Java Development Kit (JDK)

  1. Download JDK: Visit the Oracle JDK download page or OpenJDK and download the latest stable JDK for your operating system (e.g., JDK 17 or later).
  2. Install JDK: Follow the installation instructions for your OS.
    • Windows: Run the installer (.exe) and follow the prompts.
    • macOS: Run the installer (.dmg) and follow the prompts.
    • Linux: Use your distribution’s package manager (e.g., sudo apt install openjdk-17-jdk).
  3. Verify Installation: Open your terminal or command prompt and type:
    java -version
    javac -version
    
    You should see output indicating the Java version you installed.

Step 2: Install IntelliJ IDEA Community Edition

  1. Download IntelliJ IDEA: Go to the JetBrains IntelliJ IDEA download page and download the Community Edition (which is free and open-source).
  2. Install IntelliJ IDEA:
    • Windows: Run the installer (.exe) and follow the prompts.
    • macOS: Drag the application icon to your Applications folder.
    • Linux: Follow the instructions for your distribution (often via a tar.gz file or a snap package).
  3. Launch IntelliJ IDEA: Once installed, launch the IDE.

Step 3: Install Apache Maven

Maven is often bundled with IntelliJ IDEA, but it’s good practice to ensure it’s properly set up.

  1. Check for existing Maven: Open your terminal/command prompt and type:
    mvn -v
    
    If Maven is installed and configured, you’ll see its version information. If not, proceed to the next step.
  2. Download Maven: Go to the Apache Maven download page and download the binary zip archive (e.g., apache-maven-X.X.X-bin.zip).
  3. Extract Maven: Extract the downloaded zip file to a convenient location (e.g., C:\Program Files\Apache\Maven on Windows, or /opt/maven on Linux/macOS).
  4. Set Environment Variables:
    • M2_HOME: Point this to the directory where you extracted Maven (e.g., C:\Program Files\Apache\Maven\apache-maven-X.X.X).
    • MAVEN_HOME: Same as M2_HOME.
    • Path: Add %M2_HOME%\bin (Windows) or $M2_HOME/bin (Linux/macOS) to your system’s Path variable.
    • Windows: Search for “Environment Variables” in the Start Menu, go to “Edit the system environment variables” -> “Environment Variables…”
    • macOS/Linux: Edit your shell profile file (e.g., ~/.bash_profile, ~/.zshrc, ~/.bashrc) and add:
      export M2_HOME=/path/to/apache-maven-X.X.X
      export MAVEN_HOME=/path/to/apache-maven-X.X.X
      export PATH=$PATH:$M2_HOME/bin
      
      Then, source ~/.bash_profile (or your relevant file) to apply changes.
  5. Verify Installation: Re-open your terminal/command prompt and type mvn -v. You should now see Maven’s version.

Congratulations! Your development environment is now set up.

Core Concepts and Fundamentals

In this section, we’ll dive into the fundamental building blocks of Java automation testing. We’ll start with Maven project setup, then explore JUnit and TestNG as test runners, followed by the basics of Selenium WebDriver for UI and Rest Assured for API testing.

2.1. Maven Project Setup

Maven is crucial for managing project dependencies (libraries like Selenium, Rest Assured, JUnit/TestNG) and building our test projects.

Detailed Explanation

A Maven project structure is standardized, which makes it easy for developers to understand and navigate. The pom.xml (Project Object Model) file is the heart of a Maven project, defining its configuration, dependencies, plugins, and build lifecycle.

Key elements in pom.xml:

  • <groupId>, <artifactId>, <version>: Uniquely identify your project.
  • <properties>: Define reusable values like Java version, dependency versions.
  • <dependencies>: List external libraries your project needs. Maven will automatically download and manage these.
  • <build>: Configure plugins for compilation, testing, packaging, etc.

Code Example: Creating a New Maven Project

Let’s create our first Maven project in IntelliJ IDEA.

  1. Open IntelliJ IDEA.
  2. Click “New Project” on the Welcome screen, or File > New > Project... if you have a project open.
  3. In the New Project wizard:
    • Select Maven from the left pane.
    • Check “Create from Archetype” and choose maven-archetype-quickstart. This creates a basic project structure.
    • Click Next.
    • Name: JavaAutomationGuide
    • Location: Choose a directory on your computer.
    • Group Id: com.automation.guide
    • Artifact Id: JavaAutomationGuide
    • Version: 1.0-SNAPSHOT
    • Click Finish.
  4. IntelliJ will create the project and import Maven dependencies. You’ll see a pom.xml file.

Your pom.xml should look similar to this initially:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.automation.guide</groupId>
    <artifactId>JavaAutomationGuide</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <junit.version>5.10.0</junit.version> <!-- We'll use JUnit 5 as default -->
    </properties>

    <dependencies>
        <!-- JUnit Jupiter API for writing tests -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- JUnit Jupiter Engine for running tests -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Plugin to compile Java code -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <!-- Plugin to run JUnit tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
            </plugin>
        </plugins>
    </build>
</project>

Understanding scope: test: This tells Maven that these dependencies are only needed for compiling and running tests, not for the main application code itself.

Exercises/Mini-Challenges:

  1. Add a comment to your pom.xml above the <dependencies> tag, explaining its purpose.
  2. Change the maven.compiler.source and maven.compiler.target properties to 17 (if you installed JDK 17). Then, click the “Reload Maven Project” button (usually a small “M” icon with refresh arrows in IntelliJ) to apply changes.

2.2. Test Runners: JUnit 5 and TestNG

Test runners are frameworks that provide annotations and utilities to write, organize, and execute tests. We’ll briefly look at both JUnit 5 and TestNG, as they are the most popular choices in Java.

Detailed Explanation

  • JUnit 5: The successor to JUnit 4, JUnit 5 is a modular and extensible framework for writing automated tests in Java. It uses annotations (like @Test, @BeforeEach, @AfterEach) to define test methods and lifecycle hooks.
  • TestNG: (Test Next Generation) is a more powerful and flexible testing framework than JUnit (especially JUnit 4). It offers advanced features like test method prioritization, parallel execution, data-driven testing with @DataProvider, and dependency between test methods. For complex test suites, TestNG is often preferred.

For simplicity and a “learn by doing” approach, we will primarily use TestNG in this guide as it provides more features out-of-the-box for automation testing, especially for UI and API tests.

Setting up TestNG in Maven

First, let’s remove the JUnit dependencies from pom.xml and add TestNG.

Modify pom.xml:

  1. Delete the entire <dependencies> section for JUnit.
  2. Add the TestNG dependency within the <dependencies> tag.
  3. Update the maven-surefire-plugin to correctly run TestNG tests.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.automation.guide</groupId>
    <artifactId>JavaAutomationGuide</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <testng.version>7.10.2</testng.version> <!-- Latest stable TestNG version as of search results -->
    </properties>

    <dependencies>
        <!-- TestNG Dependency -->
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>${testng.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- Plugin to compile Java code -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <!-- Plugin to run TestNG tests -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version> <!-- Use the latest compatible version -->
                <configuration>
                    <suiteXmlFiles>
                        <!-- Specify your TestNG XML suite file here -->
                        <suiteXmlFile>testng.xml</suiteXmlFile>
                    </suiteXmlFiles>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Remember to click the “Reload Maven Project” button in IntelliJ IDEA after modifying pom.xml.

Code Example: Your First TestNG Test

  1. Create a Test Class: In your project, navigate to src/test/java/com/automation/guide.
  2. Delete the existing AppTest.java file (or rename it).
  3. Create a new Java class named FirstTestNGTest.java.
  4. Add the following code:
package com.automation.guide;

import org.testng.annotations.Test;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.AfterMethod;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;

public class FirstTestNGTest {

    // This method will run before each test method
    @BeforeMethod
    public void setup() {
        System.out.println("--- Setting up for a new test ---");
    }

    // This is our first test method
    @Test
    public void verifyAddition() {
        System.out.println("Running test: verifyAddition");
        int num1 = 5;
        int num2 = 10;
        int sum = num1 + num2;
        // Assert that the sum is 15
        assertEquals(sum, 15, "Sum of 5 and 10 should be 15");
        System.out.println("Addition test passed!");
    }

    // This is another test method
    @Test
    public void verifyStringContains() {
        System.out.println("Running test: verifyStringContains");
        String message = "Hello, Java Automation!";
        // Assert that the message contains "Java"
        assertTrue(message.contains("Java"), "Message should contain 'Java'");
        System.out.println("String contains test passed!");
    }

    // This method will run after each test method
    @AfterMethod
    public void tearDown() {
        System.out.println("--- Test finished, cleaning up ---");
        System.out.println(); // Add an empty line for better readability
    }
}

Running TestNG Tests

To run TestNG tests with Maven, we need a testng.xml file.

  1. Create testng.xml: In the root of your project (same level as pom.xml), create a new XML file named testng.xml.
  2. Add the following content to testng.xml:
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="MyFirstTestSuite" verbose="1" >
    <test name="BasicMathAndStringTests" >
        <classes>
            <class name="com.automation.guide.FirstTestNGTest" />
        </classes>
    </test>
</suite>

Run from IntelliJ:

  • Right-click on testng.xml and select Run 'MyFirstTestSuite'.

Run from Command Line (using Maven):

  1. Open your terminal/command prompt.
  2. Navigate to your project’s root directory (JavaAutomationGuide).
  3. Execute the Maven command:
    mvn clean test
    

You should see output similar to this, indicating two successful tests:

--- Setting up for a new test ---
Running test: verifyAddition
Addition test passed!
--- Test finished, cleaning up ---

--- Setting up for a new test ---
Running test: verifyStringContains
String contains test passed!
--- Test finished, cleaning up ---

===============================================
MyFirstTestSuite
Total tests run: 2, Passes: 2, Failures: 0, Skips: 0
===============================================

Exercises/Mini-Challenges:

  1. Create a new test method in FirstTestNGTest.java that asserts 5 * 5 equals 25. Make sure to use @Test and an assertion from org.testng.Assert.
  2. Introduce a failing assertion in one of your test methods (e.g., assertEquals(sum, 16) in verifyAddition). Run the tests and observe the failure report. Then fix it.
  3. Add a new class SecondTestNGTest.java with one simple @Test method. Update testng.xml to include both FirstTestNGTest and SecondTestNGTest in your test suite. Run the suite to execute tests from both classes.

2.3. UI Automation with Selenium WebDriver Fundamentals

Selenium WebDriver is the cornerstone of web UI automation. It allows you to programmatically control web browsers, just like a user would.

Detailed Explanation

Selenium WebDriver works by sending commands to a browser-specific driver (e.g., ChromeDriver for Chrome, GeckoDriver for Firefox). This driver then interacts with the browser to perform actions (like clicking, typing) and retrieve information (like text, element attributes).

Key Concepts:

  • WebDriver Interface: The main interface for performing browser actions.
  • Browser Drivers: Specific implementations of WebDriver for different browsers (e.g., ChromeDriver, FirefoxDriver).
  • Locators: Strategies to find web elements on a page (e.g., id, name, className, tagName, linkText, partialLinkText, cssSelector, xpath). Choosing the right locator is crucial for stable tests.
  • WebElement Interface: Represents an HTML element on the web page. Once found, you can interact with it (click, send keys, get text, etc.).
  • Waits: Handling dynamic loading of web elements is critical.
    • Implicit Wait: A global setting for the WebDriver to wait for a certain amount of time before throwing a NoSuchElementException.
    • Explicit Wait: More flexible, used to wait for a specific condition to be met (e.g., element to be visible, clickable) for a certain duration.

Code Example: Basic Browser Interaction

Let’s automate navigating to a website and interacting with a simple element. We’ll use Chrome for this example.

Prerequisites for Selenium:

  1. WebDriver Manager (Recommended): Instead of manually downloading browser drivers (like chromedriver.exe), we’ll use WebDriverManager. It automatically handles downloading and setting up the correct driver for your browser.

Add WebDriverManager and Selenium to pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.automation.guide</groupId>
    <artifactId>JavaAutomationGuide</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <testng.version>7.10.2</testng.version>
        <selenium.version>4.22.0</selenium.version> <!-- Check for latest Selenium 4.x -->
        <webdrivermanager.version>5.9.1</webdrivermanager.version> <!-- Check for latest WebDriverManager -->
    </properties>

    <dependencies>
        <!-- TestNG Dependency -->
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>${testng.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Selenium WebDriver -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>${selenium.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- WebDriverManager for automatic driver management -->
        <dependency>
            <groupId>io.github.bonigarcia</groupId>
            <artifactId>webdrivermanager</artifactId>
            <version>${webdrivermanager.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
                <configuration>
                    <suiteXmlFiles>
                        <suiteXmlFile>testng.xml</suiteXmlFile>
                    </suiteXmlFiles>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Reload your Maven project in IntelliJ.

Now, let’s write a UI test:

Create a new Java class named GoogleSearchTest.java in src/test/java/com/automation/guide.

package com.automation.guide;

import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertEquals;

import java.time.Duration;

public class GoogleSearchTest {

    WebDriver driver; // Declare WebDriver instance

    @BeforeMethod
    public void setupBrowser() {
        // Automatically download and set up ChromeDriver
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver(); // Initialize ChromeDriver
        driver.manage().window().maximize(); // Maximize browser window
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10)); // Set implicit wait
        System.out.println("Browser setup complete.");
    }

    @Test
    public void performGoogleSearch() {
        System.out.println("Starting Google Search Test...");
        driver.get("https://www.google.com"); // Navigate to Google

        // Find the search box element by its name attribute and type "Selenium WebDriver"
        WebElement searchBox = driver.findElement(By.name("q"));
        searchBox.sendKeys("Selenium WebDriver");
        searchBox.submit(); // Submit the search

        // Verify the title of the search results page
        String pageTitle = driver.getTitle();
        System.out.println("Page Title: " + pageTitle);
        assertTrue(pageTitle.contains("Selenium WebDriver"), "Page title should contain 'Selenium WebDriver'");

        // Verify that some search results are displayed (a very basic check)
        WebElement resultsStats = driver.findElement(By.id("result-stats"));
        assertTrue(resultsStats.isDisplayed(), "Search results statistics should be displayed");
        System.out.println("Google Search Test Passed!");
    }

    @AfterMethod
    public void teardownBrowser() {
        if (driver != null) {
            driver.quit(); // Close all browser windows and end WebDriver session
            System.out.println("Browser closed.");
        }
    }
}

Update testng.xml to include GoogleSearchTest:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="MyFirstTestSuite" verbose="1" >
    <test name="BasicMathAndStringTests" >
        <classes>
            <class name="com.automation.guide.FirstTestNGTest" />
            <class name="com.automation.guide.GoogleSearchTest" /> <!-- Add this line -->
        </classes>
    </test>
</suite>

Run the testng.xml file. You should see a Chrome browser open, navigate to Google, perform a search, and then close.

Exercises/Mini-Challenges:

  1. Modify performGoogleSearch to search for your favorite programming language.
  2. Add an assertion to performGoogleSearch to check that the URL after search also contains “Selenium WebDriver” (use driver.getCurrentUrl()).
  3. Explore different locators: Instead of By.name("q"), try to find the search box using a By.cssSelector or By.xpath. (Hint: Use your browser’s developer tools to inspect the element).
  4. Try navigating to another website after the Google search, e.g., driver.get("https://www.selenium.dev").

2.4. Backend Automation with Rest Assured Fundamentals

Rest Assured is a powerful Java library for making HTTP requests and validating responses, specifically designed for testing RESTful APIs.

Detailed Explanation

REST Assured provides a BDD (Behavior-Driven Development) style syntax (Given-When-Then) that makes API tests highly readable and easy to write. It handles the complexity of HTTP requests, JSON/XML parsing, and response validation, allowing you to focus on the business logic of your API.

Key Concepts:

  • Given(): Defines the prerequisites (request headers, parameters, body, authentication).
  • When(): Specifies the HTTP method and endpoint (GET, POST, PUT, DELETE).
  • Then(): Validates the response (status code, response body, headers, cookies).
  • JSONPath/XMLPath: Powerful utilities to extract specific data from JSON or XML responses for assertions.
  • Deserialization: Converting JSON/XML responses directly into Java objects (POJOs - Plain Old Java Objects).

Code Example: Basic API Testing

We’ll use a publicly available mock API for this example, ReqRes.in.

Add Rest Assured to pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.automation.guide</groupId>
    <artifactId>JavaAutomationGuide</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <testng.version>7.10.2</testng.version>
        <selenium.version>4.22.0</selenium.version>
        <webdrivermanager.version>5.9.1</webdrivermanager.version>
        <rest-assured.version>5.5.0</rest-assured.version> <!-- Check for latest Rest Assured -->
    </properties>

    <dependencies>
        <!-- TestNG Dependency -->
        <dependency>
            <groupId>org.testng</groupId>
            <artifactId>testng</artifactId>
            <version>${testng.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Selenium WebDriver -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>${selenium.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- WebDriverManager for automatic driver management -->
        <dependency>
            <groupId>io.github.bonigarcia</groupId>
            <artifactId>webdrivermanager</artifactId>
            <version>${webdrivermanager.version}</version>
            <scope>test</scope>
        </dependency>

        <!-- Rest Assured for API Testing -->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
            <version>${rest-assured.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- Add JSONPath for better JSON handling with Rest Assured -->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>json-path</artifactId>
            <version>${rest-assured.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- Add XMLPath if you plan to test XML responses -->
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>xml-path</artifactId>
            <version>${rest-assured.version}</version>
            <scope>test</scope>
        </dependency>
        <!-- Hamcrest for matchers in assertions -->
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest</artifactId>
            <version>2.2</version> <!-- A stable Hamcrest version -->
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.5</version>
                <configuration>
                    <suiteXmlFiles>
                        <suiteXmlFile>testng.xml</suiteXmlFile>
                    </suiteXmlFiles>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Reload your Maven project.

Create a new Java class named ReqResApiTest.java in src/test/java/com/automation/guide.

package com.automation.guide;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
import static org.hamcrest.Matchers.*;
import static io.restassured.RestAssured.given;
import static org.testng.Assert.assertEquals;

public class ReqResApiTest {

    @BeforeClass
    public void setupRestAssured() {
        // Set the base URI for all requests in this class
        RestAssured.baseURI = "https://reqres.in";
        System.out.println("Rest Assured base URI set to: " + RestAssured.baseURI);
    }

    @Test
    public void verifyListOfUsers() {
        System.out.println("Starting API test: Get List of Users...");

        given()
            .when()
                .get("/api/users?page=2") // Endpoint for list of users on page 2
            .then()
                .statusCode(200) // Verify status code is 200 (OK)
                .contentType(ContentType.JSON) // Verify content type is JSON
                .body("page", equalTo(2)) // Verify 'page' field in response is 2
                .body("data.size()", greaterThan(0)) // Verify 'data' array is not empty
                .body("data[0].id", equalTo(7)) // Verify first user's ID
                .body("data[0].first_name", equalTo("Michael")); // Verify first user's first name
        
        System.out.println("API test 'Get List of Users' Passed!");
    }

    @Test
    public void verifySingleUserNotFound() {
        System.out.println("Starting API test: Get Single User (Not Found)...");

        given()
            .when()
                .get("/api/users/23") // Request a user that does not exist
            .then()
                .statusCode(404) // Verify status code is 404 (Not Found)
                .body(equalTo("{}")); // Verify response body is an empty JSON object
        
        System.out.println("API test 'Get Single User (Not Found)' Passed!");
    }

    @Test
    public void createUser() {
        System.out.println("Starting API test: Create User...");

        String requestBody = "{\"name\": \"morpheus\", \"job\": \"leader\"}";

        Response response = given()
                .contentType(ContentType.JSON) // Set request content type to JSON
                .body(requestBody) // Set request body
            .when()
                .post("/api/users") // Send POST request to create user
            .then()
                .statusCode(201) // Verify status code is 201 (Created)
                .contentType(ContentType.JSON)
                .body("name", equalTo("morpheus")) // Verify name in response
                .body("job", equalTo("leader")) // Verify job in response
                .extract().response(); // Extract the full response for further assertions

        System.out.println("Created User Response: " + response.asString());
        assertEquals(response.jsonPath().getString("name"), "morpheus", "Name from response mismatch");
        assertEquals(response.jsonPath().getString("job"), "leader", "Job from response mismatch");
        System.out.println("API test 'Create User' Passed!");
    }
}

Update testng.xml to include ReqResApiTest:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="MyFirstTestSuite" verbose="1" >
    <test name="BasicTests" >
        <classes>
            <class name="com.automation.guide.FirstTestNGTest" />
            <class name="com.automation.guide.GoogleSearchTest" />
            <class name="com.automation.guide.ReqResApiTest" /> <!-- Add this line -->
        </classes>
    </test>
</suite>

Run the testng.xml file. You won’t see a browser open, as these are backend tests. The output in your console will show the API calls and their validations.

Exercises/Mini-Challenges:

  1. Add a new test method in ReqResApiTest.java to get a single user (e.g., GET /api/users/2). Assert the status code is 200 and verify the email field (e.g., janet.weaver@reqres.in).
  2. Modify the createUser test to create a user with a different name and job, and assert those new values in the response.
  3. Experiment with headers: Add a custom header to one of your requests using .header("X-Custom-Header", "MyValue") and verify its presence in the request (though ReqRes won’t return it). This is more for understanding.

3. Intermediate Topics

Now that you have a solid understanding of the fundamentals, let’s explore more advanced concepts in both UI and API automation, focusing on making your tests robust, maintainable, and efficient.

3.1. Advanced UI Automation: Page Object Model (POM) and Explicit Waits

As your UI test suite grows, managing locators and test logic becomes challenging. The Page Object Model (POM) design pattern helps organize your code, and explicit waits prevent flaky tests.

Detailed Explanation

  • Page Object Model (POM):

    • Concept: POM is a design pattern in test automation where each web page in your application has a corresponding Java class. This class contains web elements (buttons, text fields, links) as variables and methods that represent user interactions on that page.
    • Benefits:
      • Maintainability: If the UI changes, you only need to update the locator in one place (the Page Object class), not in every test case that uses that element.
      • Readability: Test cases become cleaner and easier to understand, as they interact with meaningful methods (e.g., loginPage.login("user", "pass")) instead of raw locators.
      • Reusability: Page object methods and elements can be reused across multiple test cases.
  • Explicit Waits:

    • Concept: Explicit waits tell WebDriver to pause execution only until a certain condition is met or until a maximum timeout occurs. This is more intelligent than implicit waits (which apply globally and often lead to unnecessary waits) or fixed Thread.sleep() (which are unreliable).
    • Classes: WebDriverWait for the wait mechanism and ExpectedConditions for predefined conditions (e.g., visibilityOfElementLocated, elementToBeClickable).

Code Example: Implementing POM and Explicit Waits

We’ll use a simple login page example for this. Let’s assume you have a website with a login page at https://example.com/login. (You can use a public demo site like The Internet Heroku App for practice, but we’ll simulate the structure here).

Create a new package pages under src/test/java/com/automation/guide. Create LoginPage.java inside the pages package:

package com.automation.guide.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import java.time.Duration;

public class LoginPage {
    private WebDriver driver;
    private WebDriverWait wait;

    // Locators for elements on the login page
    private By usernameField = By.id("username");
    private By passwordField = By.id("password");
    private By loginButton = By.xpath("//button[@type='submit']");
    private By flashMessage = By.id("flash"); // For success/failure messages

    // Constructor to initialize WebDriver and WebDriverWait
    public LoginPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); // 10 seconds explicit wait
    }

    // Method to navigate to the login page
    public void navigateToLoginPage(String url) {
        driver.get(url);
        System.out.println("Navigated to: " + url);
    }

    // Method to enter username
    public void enterUsername(String username) {
        WebElement usernameInput = wait.until(ExpectedConditions.visibilityOfElementLocated(usernameField));
        usernameInput.sendKeys(username);
        System.out.println("Entered username: " + username);
    }

    // Method to enter password
    public void enterPassword(String password) {
        WebElement passwordInput = wait.until(ExpectedConditions.visibilityOfElementLocated(passwordField));
        passwordInput.sendKeys(password);
        System.out.println("Entered password.");
    }

    // Method to click login button
    public void clickLoginButton() {
        WebElement loginBtn = wait.until(ExpectedConditions.elementToBeClickable(loginButton));
        loginBtn.click();
        System.out.println("Clicked login button.");
    }

    // Method to perform a complete login action
    public void login(String username, String password) {
        enterUsername(username);
        enterPassword(password);
        clickLoginButton();
        System.out.println("Attempted login with user: " + username);
    }

    // Method to get flash message text
    public String getFlashMessageText() {
        WebElement message = wait.until(ExpectedConditions.visibilityOfElementLocated(flashMessage));
        String text = message.getText();
        System.out.println("Flash message: " + text);
        return text;
    }

    // Method to check if an element is present (useful for validation)
    public boolean isFlashMessageDisplayed() {
        try {
            wait.until(ExpectedConditions.visibilityOfElementLocated(flashMessage));
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

Create a new test class named LoginTestWithPOM.java in src/test/java/com/automation/guide. We’ll use The Internet Heroku App Login Page for this example.

package com.automation.guide;

import com.automation.guide.pages.LoginPage;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertFalse;

import java.time.Duration;

public class LoginTestWithPOM {

    private WebDriver driver;
    private LoginPage loginPage; // Declare Page Object

    private final String BASE_URL = "http://the-internet.herokuapp.com/login";

    @BeforeMethod
    public void setupBrowser() {
        WebDriverManager.chromedriver().setup();
        driver = new ChromeDriver();
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(0)); // Disable implicit wait for explicit waits
        loginPage = new LoginPage(driver); // Initialize Page Object
        System.out.println("Browser and Page Object setup complete.");
    }

    @Test
    public void testSuccessfulLogin() {
        System.out.println("Starting test: Successful Login");
        loginPage.navigateToLoginPage(BASE_URL);
        loginPage.login("tomsmith", "SuperSecretPassword!");

        // Assert that the success message is displayed
        String successMessage = loginPage.getFlashMessageText();
        assertTrue(successMessage.contains("You logged into a secure area!"),
                   "Success message not displayed or incorrect.");
        System.out.println("Successful Login Test Passed!");
    }

    @Test
    public void testFailedLogin_InvalidCredentials() {
        System.out.println("Starting test: Failed Login - Invalid Credentials");
        loginPage.navigateToLoginPage(BASE_URL);
        loginPage.login("invalidUser", "wrongPassword");

        // Assert that the failure message is displayed
        String errorMessage = loginPage.getFlashMessageText();
        assertTrue(errorMessage.contains("Your username is invalid!"),
                   "Error message not displayed or incorrect.");
        System.out.println("Failed Login Test (Invalid Credentials) Passed!");
    }

    @AfterMethod
    public void teardownBrowser() {
        if (driver != null) {
            driver.quit();
            System.out.println("Browser closed after login tests.");
        }
    }
}

Update testng.xml to include LoginTestWithPOM:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="MyFirstTestSuite" verbose="1" >
    <test name="BasicTests" >
        <classes>
            <class name="com.automation.guide.FirstTestNGTest" />
            <class name="com.automation.guide.GoogleSearchTest" />
            <class name="com.automation.guide.ReqResApiTest" />
            <class name="com.automation.guide.LoginTestWithPOM" /> <!-- Add this line -->
        </classes>
    </test>
</suite>

Run the testng.xml. Observe how the tests interact with the page using the LoginPage methods and how the explicit waits ensure elements are ready before interaction. Notice how we set implicitlyWait(Duration.ofSeconds(0)) in BeforeMethod to avoid conflicts with explicit waits.

Exercises/Mini-Challenges:

  1. Add a SecureAreaPage.java class that represents the page after successful login. It should have a method to verify the “Welcome to the Secure Area” header and a method to click the “Logout” button.
  2. Integrate SecureAreaPage into testSuccessfulLogin in LoginTestWithPOM. After successful login, instantiate SecureAreaPage, verify the welcome message, then click logout and verify that you are redirected back to the login page (or the flash message indicates logout).
  3. Experiment with different ExpectedConditions in LoginPage.java. For example, try ExpectedConditions.elementToBeClickable() for the login button or ExpectedConditions.textToBePresentInElementLocated() for the flash message.

3.2. Advanced API Testing: Request Body, Deserialization, and Complex Assertions

Beyond simple GET requests, real-world API testing involves complex request bodies, extracting and using dynamic data, and more sophisticated response validations.

Detailed Explanation

  • POST/PUT Request Bodies: Sending data to the server usually involves constructing a JSON or XML payload. Rest Assured makes this easy with .body().
  • POJO (Plain Old Java Object) for Request/Response: Instead of manually building JSON strings, you can create Java classes that represent the structure of your JSON/XML data. This allows for type-safe data handling and easier serialization (Java object to JSON) and deserialization (JSON to Java object).
  • Dynamic Data Extraction and Chaining Requests: Often, you need to extract an ID or token from one API response and use it in a subsequent request. Rest Assured’s extract().path() or extract().response() methods are very useful here.
  • Complex Assertions with Hamcrest: While equalTo is simple, Hamcrest provides a rich set of matchers (e.g., containsString, hasItems, hasSize, greaterThan, lessThan) for more powerful and readable assertions on response data.

Code Example: POJOs, POST Request, and Dynamic Data

We’ll continue using ReqRes.in for this example.

Create a new package models under src/test/java/com/automation/guide. Create User.java inside the models package (POJO for a User):

package com.automation.guide.models;

// Import Jackson annotations for JSON serialization/deserialization
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

// Ignore unknown properties to avoid deserialization errors if API response has extra fields
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
    private int id;
    private String email;
    @JsonProperty("first_name") // Map JSON field "first_name" to Java field "firstName"
    private String firstName;
    @JsonProperty("last_name") // Map JSON field "last_name" to Java field "lastName"
    private String lastName;
    private String avatar;
    private String name; // For create user response
    private String job; // For create user response
    private String createdAt; // For create user response

    // Default constructor is required for deserialization
    public User() {
    }

    // Constructor for creating a new user (request body)
    public User(String name, String job) {
        this.name = name;
        this.job = job;
    }

    // Getters and Setters
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getJob() {
        return job;
    }

    public void setJob(String job) {
        this.job = job;
    }

    public String getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(String createdAt) {
        this.createdAt = createdAt;
    }

    @Override
    public String toString() {
        return "User{" +
               "id=" + id +
               ", email='" + email + '\'' +
               ", firstName='" + firstName + '\'' +
               ", lastName='" + lastName + '\'' +
               ", avatar='" + avatar + '\'' +
               ", name='" + name + '\'' +
               ", job='" + job + '\'' +
               ", createdAt='" + createdAt + '\'' +
               '}';
    }
}

Add Jackson Databind dependency for POJO serialization/deserialization. Modify pom.xml:

        <!-- Jackson Databind for JSON to POJO conversion -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.17.2</version> <!-- Check for latest stable version -->
            <scope>test</scope>
        </dependency>

Reload your Maven project.

Create a new test class named AdvancedApiTest.java in src/test/java/com/automation/guide.

package com.automation.guide;

import com.automation.guide.models.User;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

public class AdvancedApiTest {

    private String createdUserId; // To store dynamic data for chaining tests

    @BeforeClass
    public void setupRestAssured() {
        RestAssured.baseURI = "https://reqres.in";
        System.out.println("Rest Assured base URI set to: " + RestAssured.baseURI);
    }

    @Test(priority = 1) // Run this test first
    public void testCreateUserWithPOJO() {
        System.out.println("Starting API test: Create User with POJO...");

        User newUser = new User("Jane Doe", "QA Engineer");

        Response response = given()
                .contentType(ContentType.JSON)
                .body(newUser) // Rest Assured automatically serializes POJO to JSON
            .when()
                .post("/api/users")
            .then()
                .statusCode(201)
                .contentType(ContentType.JSON)
                .body("name", equalTo("Jane Doe"))
                .body("job", equalTo("QA Engineer"))
                .extract().response();

        // Deserialize response directly to a User POJO
        User createdUser = response.as(User.class);
        System.out.println("Created User (POJO): " + createdUser);

        assertNotNull(createdUser.getId(), "User ID should not be null");
        assertNotNull(createdUser.getCreatedAt(), "CreatedAt timestamp should not be null");
        assertEquals(createdUser.getName(), newUser.getName(), "Name mismatch after creation");
        assertEquals(createdUser.getJob(), newUser.getJob(), "Job mismatch after creation");

        // Store the created ID for subsequent tests
        createdUserId = createdUser.getId() != 0 ? String.valueOf(createdUser.getId()) : response.jsonPath().getString("id");
        System.out.println("Created User ID: " + createdUserId);
        assertNotNull(createdUserId, "Failed to extract created user ID.");
        System.out.println("API test 'Create User with POJO' Passed!");
    }

    @Test(priority = 2, dependsOnMethods = {"testCreateUserWithPOJO"}) // Run this after create user
    public void testGetCreatedUser() {
        System.out.println("Starting API test: Get Created User using ID " + createdUserId + "...");

        assertNotNull(createdUserId, "Created User ID is null, cannot proceed with GET test.");

        Response response = given()
            .when()
                .get("/api/users/" + createdUserId)
            .then()
                .statusCode(200)
                .contentType(ContentType.JSON)
                .extract().response();

        User fetchedUser = response.jsonPath().getObject("data", User.class);
        System.out.println("Fetched User (POJO): " + fetchedUser);

        // Note: reqres.in mock API doesn't actually store created users,
        // so we'll assert against expected data for a fixed user like ID 2.
        // In a real application, you'd assert against the data of the 'createdUser'.
        // For demonstration, let's just ensure we get a valid user back.
        assertNotNull(fetchedUser.getId(), "Fetched user ID should not be null");
        assertNotNull(fetchedUser.getEmail(), "Fetched user email should not be null");
        System.out.println("API test 'Get Created User' Passed!");
    }

    @Test(priority = 3, dependsOnMethods = {"testCreateUserWithPOJO"})
    public void testUpdateUser() {
        System.out.println("Starting API test: Update User " + createdUserId + "...");

        assertNotNull(createdUserId, "Created User ID is null, cannot proceed with PUT test.");

        User updatedUser = new User("Jane Doe Updated", "Senior QA Engineer");

        given()
                .contentType(ContentType.JSON)
                .body(updatedUser)
            .when()
                .put("/api/users/" + createdUserId)
            .then()
                .statusCode(200) // PUT usually returns 200 OK
                .contentType(ContentType.JSON)
                .body("name", equalTo("Jane Doe Updated"))
                .body("job", equalTo("Senior QA Engineer"));

        System.out.println("API test 'Update User' Passed!");
    }
}

Update testng.xml to include AdvancedApiTest:

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="MyFirstTestSuite" verbose="1" >
    <test name="BasicTests" >
        <classes>
            <class name="com.automation.guide.FirstTestNGTest" />
            <class name="com.automation.guide.GoogleSearchTest" />
            <class name="com.automation.guide.ReqResApiTest" />
            <class name="com.automation.guide.LoginTestWithPOM" />
            <class name="com.automation.guide.AdvancedApiTest" /> <!-- Add this line -->
        </classes>
    </test>
</suite>

Run the testng.xml. Notice the @Test(priority = X) and dependsOnMethods to control the order of execution, which is crucial for chaining API requests. The use of POJOs makes the request and response handling much cleaner.

Exercises/Mini-Challenges:

  1. Add a DELETE test method (testDeleteUser) that runs after testUpdateUser. Use the createdUserId to delete the user. Assert that the status code is 204 No Content (or 200 OK if the mock API responds differently).
  2. Create another POJO UserResponse.java that specifically maps the full response for a single user (e.g., when you GET /api/users/2). It might contain a data field which is a User object, and a support field which is another object with url and text. Deserialise the entire response to this UserResponse POJO.
  3. Explore more Hamcrest matchers: In verifyListOfUsers from ReqResApiTest, add an assertion to check if data[1].id is 8 and data[1].first_name is Lindsay.

4. Advanced Topics and Best Practices

As you become more proficient, focusing on advanced techniques and best practices will make your automation framework robust, scalable, and easy to maintain.

4.1. Configuration Management

Hardcoding URLs, usernames, or passwords is a bad practice. Configuration management allows you to easily switch between environments (dev, test, production) without changing code.

Detailed Explanation

  • Property Files (.properties): Simple key-value pairs to store configuration data.
  • Maven Profiles: Allow you to define different sets of configurations or dependencies based on the environment. You can activate a profile during the Maven build.

Code Example: Using Property Files

  1. Create a config.properties file: Under src/test/resources (create the resources folder if it doesn’t exist), create a new file config.properties.

    base.url.ui=http://the-internet.herokuapp.com/login
    base.url.api=https://reqres.in
    browser=chrome
    
    # Login credentials for UI tests (for demonstration, in real life use secure methods)
    ui.username=tomsmith
    ui.password=SuperSecretPassword!
    
    # Example API key (if needed)
    api.key=YOUR_API_KEY_HERE
    
  2. Create a utility class to load properties: Create a new package utils under src/test/java/com/automation/guide. Create PropertiesLoader.java in the utils package.

    package com.automation.guide.utils;
    
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.util.Properties;
    
    public class PropertiesLoader {
        private static Properties properties;
        private static final String CONFIG_FILE_PATH = "src/test/resources/config.properties";
    
        static {
            properties = new Properties();
            try (FileInputStream fis = new FileInputStream(CONFIG_FILE_PATH)) {
                properties.load(fis);
            } catch (IOException e) {
                System.err.println("Error loading configuration properties from " + CONFIG_FILE_PATH);
                e.printStackTrace();
                throw new RuntimeException("Failed to load configuration properties.", e);
            }
        }
    
        public static String getProperty(String key) {
            return properties.getProperty(key);
        }
    }
    
  3. Update LoginTestWithPOM to use properties:

    package com.automation.guide;
    
    import com.automation.guide.pages.LoginPage;
    import com.automation.guide.utils.PropertiesLoader; // Import the utility
    import io.github.bonigarcia.wdm.WebDriverManager;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.chrome.ChromeDriver;
    import org.testng.annotations.AfterMethod;
    import org.testng.annotations.BeforeMethod;
    import org.testng.annotations.Test;
    import static org.testng.Assert.assertTrue;
    
    import java.time.Duration;
    
    public class LoginTestWithPOM {
    
        private WebDriver driver;
        private LoginPage loginPage;
    
        private final String BASE_URL = PropertiesLoader.getProperty("base.url.ui"); // Get URL from properties
        private final String USERNAME = PropertiesLoader.getProperty("ui.username"); // Get username
        private final String PASSWORD = PropertiesLoader.getProperty("ui.password"); // Get password
    
        @BeforeMethod
        public void setupBrowser() {
            WebDriverManager.chromedriver().setup();
            driver = new ChromeDriver();
            driver.manage().window().maximize();
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(0));
            loginPage = new LoginPage(driver);
            System.out.println("Browser and Page Object setup complete.");
        }
    
        @Test
        public void testSuccessfulLogin() {
            System.out.println("Starting test: Successful Login");
            loginPage.navigateToLoginPage(BASE_URL);
            loginPage.login(USERNAME, PASSWORD); // Use properties
            String successMessage = loginPage.getFlashMessageText();
            assertTrue(successMessage.contains("You logged into a secure area!"),
                       "Success message not displayed or incorrect.");
            System.out.println("Successful Login Test Passed!");
        }
    
        @Test
        public void testFailedLogin_InvalidCredentials() {
            System.out.println("Starting test: Failed Login - Invalid Credentials");
            loginPage.navigateToLoginPage(BASE_URL);
            loginPage.login("wrongUser", "wrongPassword"); // Still hardcoded for negative scenario
            String errorMessage = loginPage.getFlashMessageText();
            assertTrue(errorMessage.contains("Your username is invalid!"),
                       "Error message not displayed or incorrect.");
            System.out.println("Failed Login Test (Invalid Credentials) Passed!");
        }
    
        @AfterMethod
        public void teardownBrowser() {
            if (driver != null) {
                driver.quit();
                System.out.println("Browser closed after login tests.");
            }
        }
    }
    
  4. Update ReqResApiTest and AdvancedApiTest to use properties for baseURI:

    // In ReqResApiTest and AdvancedApiTest
    @BeforeClass
    public void setupRestAssured() {
        RestAssured.baseURI = PropertiesLoader.getProperty("base.url.api"); // Get API URL from properties
        System.out.println("Rest Assured base URI set to: " + RestAssured.baseURI);
    }
    

Now, your tests fetch configuration from config.properties.

Exercises/Mini-Challenges:

  1. Create a new config_prod.properties file under src/test/resources. Change base.url.ui to https://www.google.com (or any other website) and base.url.api to a different mock API if you know one.
  2. Research Maven Profiles: How would you set up Maven profiles to easily switch between config.properties and config_prod.properties when running tests from the command line (e.g., mvn test -Pprod)? (This is a research challenge, not necessarily code implementation for now).
  3. Secure Sensitive Data: For real projects, storing credentials directly in property files is a security risk. Research ways to handle sensitive data in automation frameworks (e.g., environment variables, secret management tools).

4.2. Reporting and Logging

Good test reports and clear logs are essential for debugging failures and understanding test results.

Detailed Explanation

  • TestNG Reports: TestNG generates default HTML reports in the target/surefire-reports directory, providing an overview of passed, failed, and skipped tests.
  • Logging: Using a logging framework (like SLF4J with Logback/Log4j2) provides detailed insights into test execution. It helps in debugging by showing the flow of execution, variable values, and error messages.
  • ExtentReports (Advanced Reporting): A popular third-party reporting library that generates rich, interactive HTML reports with screenshots, custom logs, and test steps, making analysis much easier. (We’ll introduce this conceptually here, but implementing it is a bigger step for a guided project).

Code Example: Basic TestNG Reporting and Logging

Basic TestNG Report: When you run mvn clean test or execute testng.xml, TestNG automatically generates a report in target/surefire-reports. Open index.html or emailable-report.html in your browser to see a summary of your tests.

Adding Logging (SLF4J + Logback):

  1. Add Logback dependencies to pom.xml:

        <!-- Logging: SLF4J API -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.13</version> <!-- Check for latest stable version -->
            <scope>test</scope>
        </dependency>
        <!-- Logging: Logback Classic (implementation) -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.6</version> <!-- Check for latest stable version -->
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.5.6</version> <!-- Check for latest stable version -->
            <scope>test</scope>
        </dependency>
    

    Reload Maven.

  2. Create logback.xml: Under src/test/resources, create a new file logback.xml.

    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
        <!-- Console Appender -->
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>
    
        <!-- File Appender -->
        <appender name="FILE" class="ch.qos.logback.core.FileAppender">
            <file>target/automation.log</file> <!-- Log file will be in target folder -->
            <encoder>
                <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>
    
        <!-- Root logger -->
        <root level="info"> <!-- Set default logging level (trace, debug, info, warn, error) -->
            <appender-ref ref="STDOUT" />
            <appender-ref ref="FILE" />
        </root>
    
        <!-- Example of specific logger for a package -->
        <logger name="com.automation.guide" level="debug" additivity="false">
            <appender-ref ref="STDOUT" />
            <appender-ref ref="FILE" />
        </logger>
    
    </configuration>
    
  3. Integrate logging into your test classes: Modify LoginTestWithPOM.java (and other test classes) to use Logger.

    package com.automation.guide;
    
    import com.automation.guide.pages.LoginPage;
    import com.automation.guide.utils.PropertiesLoader;
    import io.github.bonigarcia.wdm.WebDriverManager;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.chrome.ChromeDriver;
    import org.testng.annotations.AfterMethod;
    import org.testng.annotations.BeforeMethod;
    import org.testng.annotations.Test;
    import static org.testng.Assert.assertTrue;
    
    import java.time.Duration;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    
    public class LoginTestWithPOM {
    
        private WebDriver driver;
        private LoginPage loginPage;
    
        private static final Logger logger = LoggerFactory.getLogger(LoginTestWithPOM.class); // Initialize logger
    
        private final String BASE_URL = PropertiesLoader.getProperty("base.url.ui");
        private final String USERNAME = PropertiesLoader.getProperty("ui.username");
        private final String PASSWORD = PropertiesLoader.getProperty("ui.password");
    
        @BeforeMethod
        public void setupBrowser() {
            logger.info("Setting up browser for test."); // Use logger
            WebDriverManager.chromedriver().setup();
            driver = new ChromeDriver();
            driver.manage().window().maximize();
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(0));
            loginPage = new LoginPage(driver);
            logger.info("Browser and Page Object setup complete.");
        }
    
        @Test
        public void testSuccessfulLogin() {
            logger.info("Starting test: Successful Login");
            loginPage.navigateToLoginPage(BASE_URL);
            loginPage.login(USERNAME, PASSWORD);
            String successMessage = loginPage.getFlashMessageText();
            assertTrue(successMessage.contains("You logged into a secure area!"),
                       "Success message not displayed or incorrect.");
            logger.info("Successful Login Test Passed!");
        }
    
        @Test
        public void testFailedLogin_InvalidCredentials() {
            logger.info("Starting test: Failed Login - Invalid Credentials");
            loginPage.navigateToLoginPage(BASE_URL);
            loginPage.login("wrongUser", "wrongPassword");
            String errorMessage = loginPage.getFlashMessageText();
            assertTrue(errorMessage.contains("Your username is invalid!"),
                       "Error message not displayed or incorrect.");
            logger.info("Failed Login Test (Invalid Credentials) Passed!");
        }
    
        @AfterMethod
        public void teardownBrowser() {
            if (driver != null) {
                driver.quit();
                logger.info("Browser closed after login tests.");
            }
        }
    }
    

    Also update LoginPage to use logger instead of System.out.println for better log management.

    package com.automation.guide.pages;
    
    import org.openqa.selenium.By;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.WebElement;
    import org.openqa.selenium.support.ui.ExpectedConditions;
    import org.openqa.selenium.support.ui.WebDriverWait;
    
    import java.time.Duration;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class LoginPage {
        private WebDriver driver;
        private WebDriverWait wait;
        private static final Logger logger = LoggerFactory.getLogger(LoginPage.class);
    
        // Locators
        private By usernameField = By.id("username");
        private By passwordField = By.id("password");
        private By loginButton = By.xpath("//button[@type='submit']");
        private By flashMessage = By.id("flash");
    
        public LoginPage(WebDriver driver) {
            this.driver = driver;
            this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
        }
    
        public void navigateToLoginPage(String url) {
            driver.get(url);
            logger.info("Navigated to: {}", url); // Use logger.info with placeholders
        }
    
        public void enterUsername(String username) {
            WebElement usernameInput = wait.until(ExpectedConditions.visibilityOfElementLocated(usernameField));
            usernameInput.sendKeys(username);
            logger.info("Entered username: {}", username);
        }
    
        public void enterPassword(String password) {
            WebElement passwordInput = wait.until(ExpectedConditions.visibilityOfElementLocated(passwordField));
            passwordInput.sendKeys(password);
            logger.info("Entered password.");
        }
    
        public void clickLoginButton() {
            WebElement loginBtn = wait.until(ExpectedConditions.elementToBeClickable(loginButton));
            loginBtn.click();
            logger.info("Clicked login button.");
        }
    
        public void login(String username, String password) {
            enterUsername(username);
            enterPassword(password);
            clickLoginButton();
            logger.info("Attempted login with user: {}", username);
        }
    
        public String getFlashMessageText() {
            WebElement message = wait.until(ExpectedConditions.visibilityOfElementLocated(flashMessage));
            String text = message.getText();
            logger.info("Flash message: {}", text);
            return text;
        }
    
        public boolean isFlashMessageDisplayed() {
            try {
                wait.until(ExpectedConditions.visibilityOfElementLocated(flashMessage));
                return true;
            } catch (Exception e) {
                logger.debug("Flash message element not displayed.", e); // Log as debug if not found
                return false;
            }
        }
    }
    

Now, when you run your tests, messages will be logged to both the console and the target/automation.log file, providing a more structured and manageable way to track test execution.

Exercises/Mini-Challenges:

  1. Change the root logger level in logback.xml to debug. Rerun tests and observe more detailed output, including internal Selenium and Rest Assured logs. Then change it back to info.
  2. Add a logger.error() call inside a catch block (e.g., in isFlashMessageDisplayed() if ExpectedConditions fails) to see how error messages are handled in logs.
  3. Research ExtentReports: Explore its features and how it enhances reporting compared to default TestNG reports. (No implementation required, just understanding its value).

4.3. Data-Driven Testing (DDT) with TestNG @DataProvider

Data-Driven Testing allows you to run the same test method multiple times with different sets of input data, making your tests more comprehensive and efficient.

Detailed Explanation

TestNG’s @DataProvider annotation is a powerful way to implement DDT. A method annotated with @DataProvider returns a 2D array of objects, where each inner array represents a set of test data for one iteration of the test. The test method then accepts these parameters.

Code Example: Data-Driven Login Test

Let’s modify our LoginTestWithPOM to be data-driven for different login scenarios.

  1. Update LoginTestWithPOM.java:

    package com.automation.guide;
    
    import com.automation.guide.pages.LoginPage;
    import com.automation.guide.utils.PropertiesLoader;
    import io.github.bonigarcia.wdm.WebDriverManager;
    import org.openqa.selenium.WebDriver;
    import org.openqa.selenium.chrome.ChromeDriver;
    import org.testng.annotations.AfterMethod;
    import org.testng.annotations.BeforeMethod;
    import org.testng.annotations.DataProvider;
    import org.testng.annotations.Test;
    import static org.testng.Assert.assertTrue;
    import static org.testng.Assert.assertFalse;
    
    import java.time.Duration;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    
    public class LoginTestWithPOM {
    
        private WebDriver driver;
        private LoginPage loginPage;
    
        private static final Logger logger = LoggerFactory.getLogger(LoginTestWithPOM.class);
    
        private final String BASE_URL = PropertiesLoader.getProperty("base.url.ui");
        private final String VALID_USERNAME = PropertiesLoader.getProperty("ui.username");
        private final String VALID_PASSWORD = PropertiesLoader.getProperty("ui.password");
    
        @BeforeMethod
        public void setupBrowser() {
            logger.info("Setting up browser for test.");
            WebDriverManager.chromedriver().setup();
            driver = new ChromeDriver();
            driver.manage().window().maximize();
            driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(0));
            loginPage = new LoginPage(driver);
            logger.info("Browser and Page Object setup complete.");
        }
    
        @DataProvider(name = "loginData")
        public Object[][] getLoginData() {
            // Data: username, password, expectedMessagePart, isSuccessful
            return new Object[][]{
                    {VALID_USERNAME, VALID_PASSWORD, "You logged into a secure area!", true},
                    {"invalid", "password", "Your username is invalid!", false},
                    {VALID_USERNAME, "wrong_password", "Your username is invalid!", false}, // The Heroku app returns "Your username is invalid!" for wrong password too.
                    {"", VALID_PASSWORD, "Your username is invalid!", false}
            };
        }
    
        @Test(dataProvider = "loginData")
        public void testLoginScenarios(String username, String password, String expectedMessagePart, boolean isSuccessful) {
            logger.info("Starting login test with username: {} and password: {}", username, password);
            loginPage.navigateToLoginPage(BASE_URL);
            loginPage.login(username, password);
    
            String actualMessage = loginPage.getFlashMessageText();
    
            if (isSuccessful) {
                assertTrue(actualMessage.contains(expectedMessagePart),
                           "Login should be successful but message mismatch. Actual: " + actualMessage);
                logger.info("Login successful for user: {}", username);
                // Optional: Logout here if you want to keep tests independent
                // new SecureAreaPage(driver).logout();
            } else {
                assertTrue(actualMessage.contains(expectedMessagePart),
                           "Login should fail but message mismatch. Actual: " + actualMessage);
                logger.info("Login failed as expected for user: {}", username);
            }
            logger.info("Login test with user '{}' PASSED.", username);
        }
    
        @AfterMethod
        public void teardownBrowser() {
            if (driver != null) {
                driver.quit();
                logger.info("Browser closed after login tests.");
            }
        }
    }
    

Run testng.xml. You will see testLoginScenarios executed four times, once for each row of data in the loginData data provider.

Exercises/Mini-Challenges:

  1. Expand getLoginData: Add more test cases, such as an empty username, empty password, or both empty. Observe the application’s behavior and update the expectedMessagePart accordingly.
  2. Externalize data: Instead of hardcoding data in getLoginData, research how to read test data from an external source like a CSV file or an Excel sheet. (This is a research challenge, not implementation).
  3. Data-Driven API Test: Apply the @DataProvider concept to one of your ReqResApiTest methods. For example, create a data provider for different user IDs to GET /api/users/{id} and assert their first names.

4.4. Best Practices: Framework Design and Maintenance

Building a scalable and maintainable automation framework requires adherence to certain best practices.

Detailed Explanation

  • Modular Design: Break down your framework into small, independent modules (e.g., base, pages, utils, tests).
  • Encapsulation: Keep the internal implementation details hidden. Page Objects encapsulate UI details; utility methods encapsulate common tasks.
  • Abstraction: Hide complex underlying logic. The WebDriver interface abstracts away browser specifics; Rest Assured abstracts HTTP calls.
  • Don’t Repeat Yourself (DRY): Reuse code wherever possible (e.g., BaseTest class for common setup/teardown, utility methods).
  • Meaningful Naming: Use clear and descriptive names for classes, methods, and variables.
  • Error Handling: Implement robust error handling (try-catch blocks, custom exceptions).
  • Version Control: Use Git (or similar) to manage your code, allowing collaboration, tracking changes, and reverting to previous versions.
  • Continuous Integration (CI): Integrate your tests into a CI pipeline (Jenkins, GitHub Actions, GitLab CI) to run them automatically on every code commit.

Common Pitfalls to Avoid:

  • Excessive Thread.sleep(): Leads to flaky and slow tests. Always use explicit waits.
  • Fragile Locators: Avoid long XPath or CSS selectors that break easily with UI changes. Prefer id, name, unique class names, or attributes like data-testid.
  • Large, Monolithic Tests: Keep tests focused on a single responsibility.
  • Ignoring Failures: Investigate and fix flaky tests immediately.
  • Not Maintaining Tests: Outdated tests are useless. Regularly review and update your test suite.

Real-world Context:

In a professional setting, an automation framework typically includes:

  • Base Test Class: Handles WebDriver initialization/teardown, common utilities.
  • Page Object Model: Organizes UI elements and interactions.
  • API Client Layer: Manages API requests and responses (similar to how Rest Assured is used).
  • Utilities: Helpers for file I/O, data generation, assertions.
  • Configuration Files: Environment-specific settings.
  • Reporting: Detailed, human-readable reports (like ExtentReports).
  • CI/CD Integration: Automated execution on every build.

We’ve already laid the groundwork for many of these in the “Core Concepts” and “Intermediate Topics” sections. The Guided Projects will further solidify this understanding.

5. Guided Projects

Learning by doing is most effective! Here are two guided projects to solidify your understanding of UI and API automation.

Project 1: Automated E-commerce Product Search and Add to Cart (UI)

Objective: Simulate a user searching for a product on an e-commerce website and adding it to their cart.

Problem Statement: An e-commerce site needs its core functionality (search, product detail view, add to cart) to be robust. Automate a test flow for this.

Website to use: We’ll use a public demo e-commerce site like Demoblaze for this project.

Project Structure (Recap and New):

  • pom.xml: Configure dependencies.
  • utils/PropertiesLoader.java: Load configuration.
  • pages/: Package for Page Objects (HomePage, ProductPage, CartPage).
  • tests/: Package for TestNG test classes (DemoblazeE2ETest).
  • testng.xml: Define test suite.

Step-by-Step Guide

Step 1: Update pom.xml and config.properties Ensure your pom.xml has Selenium, WebDriverManager, TestNG, SLF4J/Logback, and Jackson Databind. Update config.properties for Demoblaze:

base.url.demoblaze=https://www.demoblaze.com/
browser=chrome
# Add any other config needed, e.g., wait times

Step 2: Create a BaseTest class for WebDriver setup/teardown This class will handle common setup and teardown for all UI tests. Create base/BaseTest.java (create base package under src/test/java/com/automation/guide).

package com.automation.guide.base;

import com.automation.guide.utils.PropertiesLoader;
import io.github.bonigarcia.wdm.WebDriverManager;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;

public class BaseTest {

    protected WebDriver driver;
    protected static final Logger logger = LoggerFactory.getLogger(BaseTest.class);

    @BeforeMethod
    public void setupDriver() {
        String browser = PropertiesLoader.getProperty("browser").toLowerCase();
        logger.info("Initializing WebDriver for browser: {}", browser);

        switch (browser) {
            case "chrome":
                WebDriverManager.chromedriver().setup();
                driver = new ChromeDriver();
                break;
            case "firefox":
                WebDriverManager.firefoxdriver().setup();
                driver = new FirefoxDriver();
                break;
            default:
                logger.error("Unsupported browser specified in config: {}", browser);
                throw new IllegalArgumentException("Unsupported browser: " + browser);
        }

        driver.manage().window().maximize();
        // Set implicit wait to 0 to rely on explicit waits in Page Objects
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(0));
        logger.info("WebDriver setup complete. Browser maximized and implicit wait disabled.");
    }

    @AfterMethod
    public void quitDriver() {
        if (driver != null) {
            driver.quit();
            logger.info("WebDriver quit.");
        }
    }
}

Important: Update PropertiesLoader.java to handle the CONFIG_FILE_PATH more robustly by getting it from the classpath, or ensure src/test/resources is in your classpath. For simplicity, we stick to the explicit path for now, but be aware of this for production systems.

Step 3: Create Page Objects Create the following classes in the pages package:

HomePage.java

package com.automation.guide.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;

public class HomePage {
    private WebDriver driver;
    private WebDriverWait wait;
    private static final Logger logger = LoggerFactory.getLogger(HomePage.class);

    // Locators
    private By productLinkByName(String productName) {
        return By.xpath("//a[contains(@class, 'hrefch') and text()='" + productName + "']");
    }
    private By categoriesSection = By.id("cat");
    private By cartLink = By.id("cartur"); // Link to Cart
    private By loginLink = By.id("login2"); // Login link (if needed later)
    private By signupLink = By.id("signin2"); // Signup link (if needed later)

    public HomePage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(15));
    }

    public void navigateToHome(String url) {
        driver.get(url);
        wait.until(ExpectedConditions.visibilityOfElementLocated(categoriesSection));
        logger.info("Navigated to Demoblaze Home Page: {}", url);
    }

    public void clickProduct(String productName) {
        WebElement product = wait.until(ExpectedConditions.elementToBeClickable(productLinkByName(productName)));
        product.click();
        logger.info("Clicked on product: {}", productName);
    }

    public void clickCartLink() {
        wait.until(ExpectedConditions.elementToBeClickable(cartLink)).click();
        logger.info("Clicked on Cart link.");
    }
}

ProductPage.java

package com.automation.guide.pages;

import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;

public class ProductPage {
    private WebDriver driver;
    private WebDriverWait wait;
    private static final Logger logger = LoggerFactory.getLogger(ProductPage.class);

    // Locators
    private By productTitle = By.cssSelector(".product-information h2");
    private By addToCartButton = By.xpath("//a[text()='Add to cart']");
    private By productPrice = By.cssSelector(".product-information h3");

    public ProductPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    public String getProductTitle() {
        WebElement titleElement = wait.until(ExpectedConditions.visibilityOfElementLocated(productTitle));
        String title = titleElement.getText();
        logger.info("Product title: {}", title);
        return title;
    }

    public String getProductPrice() {
        WebElement priceElement = wait.until(ExpectedConditions.visibilityOfElementLocated(productPrice));
        String price = priceElement.getText();
        logger.info("Product price: {}", price);
        return price;
    }

    public void clickAddToCart() {
        wait.until(ExpectedConditions.elementToBeClickable(addToCartButton)).click();
        logger.info("Clicked 'Add to cart' button.");
    }

    public String getAlertTextAndAccept() {
        wait.until(ExpectedConditions.alertIsPresent());
        Alert alert = driver.switchTo().alert();
        String alertText = alert.getText();
        alert.accept();
        logger.info("Alert appeared with text: '{}'. Accepted alert.", alertText);
        return alertText;
    }
}

CartPage.java

package com.automation.guide.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.Duration;
import java.util.List;

public class CartPage {
    private WebDriver driver;
    private WebDriverWait wait;
    private static final Logger logger = LoggerFactory.getLogger(CartPage.class);

    // Locators
    private By cartTable = By.id("tbodyid");
    private By cartRows = By.cssSelector("#tbodyid tr");
    private By totalAmountText = By.id("totalp");
    private By placeOrderButton = By.xpath("//button[text()='Place Order']");

    public CartPage(WebDriver driver) {
        this.driver = driver;
        this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
    }

    public boolean isProductInCart(String productName) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(cartTable));
        List<WebElement> rows = driver.findElements(cartRows);
        for (WebElement row : rows) {
            if (row.getText().contains(productName)) {
                logger.info("Product '{}' found in cart.", productName);
                return true;
            }
        }
        logger.warn("Product '{}' NOT found in cart.", productName);
        return false;
    }

    public int getTotalCartAmount() {
        wait.until(ExpectedConditions.visibilityOfElementLocated(totalAmountText));
        String totalText = driver.findElement(totalAmountText).getText();
        int total = Integer.parseInt(totalText);
        logger.info("Total cart amount: {}", total);
        return total;
    }

    public void clickPlaceOrder() {
        wait.until(ExpectedConditions.elementToBeClickable(placeOrderButton)).click();
        logger.info("Clicked 'Place Order' button.");
    }
}

Step 4: Create the Test Class Create DemoblazeE2ETest.java in the com.automation.guide package.

package com.automation.guide;

import com.automation.guide.base.BaseTest;
import com.automation.guide.pages.CartPage;
import com.automation.guide.pages.HomePage;
import com.automation.guide.pages.ProductPage;
import com.automation.guide.utils.PropertiesLoader;
import org.testng.annotations.Test;

import static org.testng.Assert.assertTrue;
import static org.testng.Assert.assertEquals;

public class DemoblazeE2ETest extends BaseTest { // Extend BaseTest

    private final String DEMOBLAZE_URL = PropertiesLoader.getProperty("base.url.demoblaze");

    @Test
    public void testProductSearchAndAddToCart() {
        logger.info("Starting E-commerce product search and add to cart test.");

        HomePage homePage = new HomePage(driver);
        ProductPage productPage = new ProductPage(driver);
        CartPage cartPage = new CartPage(driver);

        String productName = "Samsung galaxy s6"; // Product to search for
        String expectedPrice = "$360"; // Expected price (can be dynamic if needed)

        // 1. Navigate to home page
        homePage.navigateToHome(DEMOBLAZE_URL);

        // 2. Click on a product
        homePage.clickProduct(productName);

        // 3. Verify product details and add to cart
        assertEquals(productPage.getProductTitle(), productName, "Product title mismatch on product page.");
        assertTrue(productPage.getProductPrice().contains(expectedPrice), "Product price mismatch.");
        productPage.clickAddToCart();
        assertEquals(productPage.getAlertTextAndAccept(), "Product added.", "Alert message mismatch.");

        // 4. Navigate to cart
        homePage.clickCartLink();

        // 5. Verify product is in cart and total amount
        assertTrue(cartPage.isProductInCart(productName), "Product was not found in the cart!");
        assertEquals(cartPage.getTotalCartAmount(), 360, "Total cart amount mismatch."); // Check integer value

        // 6. Optionally, proceed to place order (not fully implemented in this example)
        cartPage.clickPlaceOrder();
        // Here you would interact with the 'Place Order' modal and verify fields.
        logger.info("E-commerce product search and add to cart test completed successfully.");
    }
}

Step 5: Update testng.xml Remove old tests from testng.xml and add only the new DemoblazeE2ETest to keep it focused.

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="DemoblazeECommerceSuite" verbose="1" >
    <test name="ProductSearchAndCartTest" >
        <classes>
            <class name="com.automation.guide.DemoblazeE2ETest" />
        </classes>
    </test>
</suite>

Run the testng.xml. This project demonstrates a full end-to-end UI flow using POM and BaseTest.

Encouragement for Independent Problem-Solving:

  • Try to implement the “Place Order” modal interaction yourself. After cartPage.clickPlaceOrder(), a modal appears. Try to:
    • Find the input fields (name, country, city, credit card, month, year).
    • Enter sample data into these fields.
    • Click the “Purchase” button.
    • Verify the success message (e.g., “Thank you for your purchase!”).
    • Close the success modal.
  • Add another test method that tests adding two different products to the cart and verifies the total amount.

Project 2: API Integration Testing for User Management (Backend)

Objective: Create, retrieve, update, and delete (CRUD) a user via a REST API.

Problem Statement: Ensure the user management API endpoints are working correctly and data consistency is maintained across operations.

API to use: We’ll continue with ReqRes.in for its simplicity, although as noted, it’s a mock API and might not perfectly simulate persistence for all operations.

Step-by-Step Guide

Step 1: Update pom.xml and config.properties Ensure pom.xml has TestNG, Rest Assured, Jackson Databind, and Hamcrest. Ensure config.properties has base.url.api=https://reqres.in.

Step 2: Reuse/Enhance User.java POJO The User.java POJO created earlier in Section 3.2 is sufficient. Ensure it includes properties for name, job, id, and createdAt (for response parsing).

Step 3: Create a BaseAPITest class (optional but good practice) For API tests, a base class might set RestAssured.baseURI or common headers. We already do this in the @BeforeClass of our existing API tests, so we can build upon that.

Step 4: Create the Test Class for CRUD operations Create UserCrudApiTest.java in src/test/java/com/automation.guide package.

package com.automation.guide;

import com.automation.guide.models.User;
import com.automation.guide.utils.PropertiesLoader;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

public class UserCrudApiTest {

    private String userId; // To store the ID of the created user for chaining
    private final String BASE_API_URL = PropertiesLoader.getProperty("base.url.api");
    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(UserCrudApiTest.class);


    @BeforeClass
    public void setup() {
        RestAssured.baseURI = BASE_API_URL;
        logger.info("Base API URI set to: {}", BASE_API_URL);
    }

    @Test(priority = 1)
    public void testCreateUser() {
        logger.info("--- Starting Test: Create a new user ---");
        User newUserRequest = new User("Alice Wonderland", "tester");

        Response response = given()
                .contentType(ContentType.JSON)
                .body(newUserRequest) // Serialize POJO to JSON
            .when()
                .post("/api/users")
            .then()
                .statusCode(201) // Verify status code is 201 (Created)
                .contentType(ContentType.JSON)
                .body("name", equalTo(newUserRequest.getName()))
                .body("job", equalTo(newUserRequest.getJob()))
                .extract().response();

        User createdUserResponse = response.as(User.class); // Deserialize JSON to POJO
        userId = createdUserResponse.getId() != 0 ? String.valueOf(createdUserResponse.getId()) : response.jsonPath().getString("id"); // Extract ID

        assertNotNull(userId, "User ID should be generated and not null");
        assertNotNull(createdUserResponse.getCreatedAt(), "CreatedAt timestamp should not be null");
        logger.info("User created successfully with ID: {}", userId);
        logger.info("Created user response: {}", response.asString());
    }

    @Test(priority = 2, dependsOnMethods = {"testCreateUser"})
    public void testGetUser() {
        logger.info("--- Starting Test: Get the created user (ID: {}) ---", userId);

        // Note: Reqres.in does not persist created data.
        // So, fetching by 'userId' from previous step will likely return mock data or 404.
        // For a real API, this would fetch the user created in testCreateUser.
        // For demonstration, we'll try to get the created ID, but expect typical Reqres behavior.

        given()
            .when()
                .get("/api/users/" + userId) // Fetch by the ID obtained
            .then()
                .statusCode(200) // Expecting 200 for existing mock users, or 404 if it tries to persist
                .contentType(ContentType.JSON)
                // For Reqres, we'll likely get a default user if ID is '1' or '2', otherwise mock data.
                // Or if it attempts to simulate non-existence, then 404.
                // Let's assume a valid ID here for the assertion structure, even if it's mock.
                .body("data.id", notNullValue()); // Just ensure we get some data back
        logger.info("Successfully attempted to get user with ID: {}", userId);
    }

    @Test(priority = 3, dependsOnMethods = {"testCreateUser"})
    public void testUpdateUser() {
        logger.info("--- Starting Test: Update the created user (ID: {}) ---", userId);
        User updatedUserRequest = new User("Alice Jane", "senior tester");

        given()
                .contentType(ContentType.JSON)
                .body(updatedUserRequest)
            .when()
                .put("/api/users/" + userId) // Use PUT for full update
            .then()
                .statusCode(200) // Expect 200 OK
                .contentType(ContentType.JSON)
                .body("name", equalTo(updatedUserRequest.getName()))
                .body("job", equalTo(updatedUserRequest.getJob()));
        logger.info("User updated successfully with ID: {}", userId);

        // Patch request example (partial update)
        logger.info("--- Starting Test: Partially update the created user (ID: {}) ---", userId);
        String partialUpdateBody = "{\"job\": \"lead QA\"}";
        given()
                .contentType(ContentType.JSON)
                .body(partialUpdateBody)
            .when()
                .patch("/api/users/" + userId) // Use PATCH for partial update
            .then()
                .statusCode(200) // Expect 200 OK
                .contentType(ContentType.JSON)
                .body("job", equalTo("lead QA"));
        logger.info("User partially updated successfully with ID: {}", userId);
    }

    @Test(priority = 4, dependsOnMethods = {"testCreateUser"})
    public void testDeleteUser() {
        logger.info("--- Starting Test: Delete the created user (ID: {}) ---", userId);

        given()
            .when()
                .delete("/api/users/" + userId)
            .then()
                .statusCode(204); // Expect 204 No Content for successful deletion
        logger.info("User deleted successfully with ID: {}", userId);

        // Optional: Verify user is truly deleted (GET request should return 404)
        logger.info("--- Verifying deletion for user (ID: {}) ---", userId);
        given()
            .when()
                .get("/api/users/" + userId)
            .then()
                .statusCode(404); // User should no longer be found
        logger.info("Deletion verified: User with ID {} is no longer found.", userId);
    }
}

Step 5: Update testng.xml Remove old API tests and add UserCrudApiTest.

<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd" >

<suite name="UserManagementApiSuite" verbose="1" >
    <test name="UserCrudOperationsTest" >
        <classes>
            <class name="com.automation.guide.UserCrudApiTest" />
        </classes>
    </test>
</suite>

Run the testng.xml. This project demonstrates a complete API CRUD flow, including POJO serialization, chaining requests using dynamically extracted data, and different HTTP methods (POST, GET, PUT, PATCH, DELETE).

Encouragement for Independent Problem-Solving:

  • Implement data-driven user creation: Use an @DataProvider to create multiple users with different names and jobs in testCreateUser. For this to work well with chained tests, you’d need to modify userId to be a collection or manage IDs in a more complex way. For a simpler challenge, just create multiple users and verify their creation within the data-driven testCreateUser method without chaining them to the same GET/PUT/DELETE sequence.
  • Handle more complex API responses: If the API had nested JSON objects or arrays, use JSONPath expressions to extract specific data points and assert them. For example, response.jsonPath().getString("data.address.street").
  • Add authentication: If an API requires a bearer token or basic authentication, research how to add it to your given() section in Rest Assured (e.g., .auth().oauth2("your_token") or .auth().preemptive().basic("user", "pass")).

6. Bonus Section: Further Learning and Resources

Congratulations on completing this comprehensive guide! You’ve built a strong foundation in Java UI and API automation testing. The journey doesn’t end here; continuous learning is key in the fast-evolving world of technology.

Here are some recommended resources to further your expertise:

  • Udemy/Coursera/Pluralsight: Look for courses on “Selenium WebDriver with Java,” “Rest Assured API Automation,” or “Test Automation Framework Design.” Instructors like Raghav Pal, Rahul Shetty, and Bas Dijkstra are highly regarded.
  • Automation Step-by-Step (Raghav Pal): Offers free and paid tutorials on various automation topics on YouTube and his website.
  • ExecuteAutomation: Another popular YouTube channel and blog for practical automation tutorials.
  • Test Automation University (Applitools): Offers a wide range of free courses taught by industry experts on various testing topics, including advanced Selenium, API testing, and framework design.

Official Documentation:

Blogs and Articles:

  • Automation Panda (Andy Knight): https://automationpanda.com/ - Excellent articles on test automation strategy, best practices, and new technologies.
  • ThoughtWorks Insights: https://www.thoughtworks.com/insights - Often features insightful articles on software quality and testing.
  • Baeldung: https://www.baeldung.com/java-rest-assured-tutorial - Comprehensive Java tutorials, including Rest Assured.
  • Various Testing Blogs: Many companies like LambdaTest, BrowserStack, Katalon (as found in web search) have excellent blogs on automation testing trends, best practices, and tool comparisons.

YouTube Channels:

Community Forums/Groups:

  • Stack Overflow: For specific programming and tool-related questions. Tag your questions with java, selenium-webdriver, rest-assured, testng.
  • Selenium Slack Channel/Google Group: Connect with other Selenium users.
  • Test Automation Guild: A global community of test automation professionals.
  • Local Meetup Groups: Search for “Test Automation,” “QA,” or “Java” meetups in your area.

Next Steps/Advanced Topics:

Once you’re comfortable with the concepts in this guide, consider exploring:

  1. Test Automation Framework Design: Deep dive into advanced framework architectures (e.g., BDD with Cucumber/Gherkin, Keyword-Driven, Hybrid Frameworks).
  2. Continuous Integration/Continuous Delivery (CI/CD): Learn to integrate your automation tests into pipelines using tools like Jenkins, GitLab CI/CD, GitHub Actions, Azure DevOps.
  3. Advanced UI Interactions: Handling complex elements like drag-and-drop, rich text editors, file uploads, Shadow DOM.
  4. Performance Testing: Tools like JMeter or Gatling can be integrated with your API tests.
  5. Security Testing (API): Tools like OWASP ZAP or Burp Suite to find vulnerabilities in your APIs.
  6. Mobile Automation (Appium): Extend your UI testing skills to native and hybrid mobile applications using Appium.
  7. Cloud Testing Platforms: Learn to run your Selenium tests on cloud grids like BrowserStack, Sauce Labs, or LambdaTest for extensive cross-browser/device testing.
  8. Containerization (Docker/Kubernetes): Use Docker to create consistent, isolated test environments for your automation suite.
  9. Reporting with ExtentReports or Allure Reports: Create visually appealing and comprehensive test reports.
  10. Design Patterns in Test Automation: Beyond POM, explore patterns like Factory, Singleton, Strategy in the context of automation.

Keep practicing, keep building, and never stop learning. Happy automating!