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:
- Java Development Kit (JDK) 11 or higher: Java is the foundation.
- An Integrated Development Environment (IDE): IntelliJ IDEA Community Edition is highly recommended for its excellent Java support.
- 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)
- 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).
- 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).
- Verify Installation: Open your terminal or command prompt and type:You should see output indicating the Java version you installed.
java -version javac -version
Step 2: Install IntelliJ IDEA Community Edition
- Download IntelliJ IDEA: Go to the JetBrains IntelliJ IDEA download page and download the Community Edition (which is free and open-source).
- 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).
- 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.
- Check for existing Maven: Open your terminal/command prompt and type:If Maven is installed and configured, you’ll see its version information. If not, proceed to the next step.
mvn -v - Download Maven: Go to the Apache Maven download page and download the binary zip archive (e.g.,
apache-maven-X.X.X-bin.zip). - Extract Maven: Extract the downloaded zip file to a convenient location (e.g.,
C:\Program Files\Apache\Mavenon Windows, or/opt/mavenon Linux/macOS). - 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 asM2_HOME.Path: Add%M2_HOME%\bin(Windows) or$M2_HOME/bin(Linux/macOS) to your system’sPathvariable.- 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:Then,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/binsource ~/.bash_profile(or your relevant file) to apply changes.
- 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.
- Open IntelliJ IDEA.
- Click “New Project” on the Welcome screen, or
File > New > Project...if you have a project open. - 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.
- IntelliJ will create the project and import Maven dependencies. You’ll see a
pom.xmlfile.
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:
- Add a comment to your
pom.xmlabove the<dependencies>tag, explaining its purpose. - Change the
maven.compiler.sourceandmaven.compiler.targetproperties to17(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:
- Delete the entire
<dependencies>section for JUnit. - Add the TestNG dependency within the
<dependencies>tag. - Update the
maven-surefire-pluginto 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
- Create a Test Class: In your project, navigate to
src/test/java/com/automation/guide. - Delete the existing
AppTest.javafile (or rename it). - Create a new Java class named
FirstTestNGTest.java. - 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.
- Create
testng.xml: In the root of your project (same level aspom.xml), create a new XML file namedtestng.xml. - 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.xmland selectRun 'MyFirstTestSuite'.
Run from Command Line (using Maven):
- Open your terminal/command prompt.
- Navigate to your project’s root directory (
JavaAutomationGuide). - 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:
- Create a new test method in
FirstTestNGTest.javathat asserts5 * 5equals25. Make sure to use@Testand an assertion fromorg.testng.Assert. - Introduce a failing assertion in one of your test methods (e.g.,
assertEquals(sum, 16)inverifyAddition). Run the tests and observe the failure report. Then fix it. - Add a new class
SecondTestNGTest.javawith one simple@Testmethod. Updatetestng.xmlto include bothFirstTestNGTestandSecondTestNGTestin 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:
WebDriverInterface: The main interface for performing browser actions.- Browser Drivers: Specific implementations of
WebDriverfor 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. WebElementInterface: 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.
- Implicit Wait: A global setting for the WebDriver to wait for a certain amount of time before throwing a
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:
- 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:
- Modify
performGoogleSearchto search for your favorite programming language. - Add an assertion to
performGoogleSearchto check that the URL after search also contains “Selenium WebDriver” (usedriver.getCurrentUrl()). - Explore different locators: Instead of
By.name("q"), try to find the search box using aBy.cssSelectororBy.xpath. (Hint: Use your browser’s developer tools to inspect the element). - 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:
- Add a new test method in
ReqResApiTest.javato get a single user (e.g., GET/api/users/2). Assert the status code is 200 and verify theemailfield (e.g.,janet.weaver@reqres.in). - Modify the
createUsertest to create a user with a different name and job, and assert those new values in the response. - 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:
WebDriverWaitfor the wait mechanism andExpectedConditionsfor predefined conditions (e.g.,visibilityOfElementLocated,elementToBeClickable).
- 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
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:
- Add a
SecureAreaPage.javaclass 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. - Integrate
SecureAreaPageintotestSuccessfulLogininLoginTestWithPOM. After successful login, instantiateSecureAreaPage, verify the welcome message, then click logout and verify that you are redirected back to the login page (or the flash message indicates logout). - Experiment with different
ExpectedConditionsinLoginPage.java. For example, tryExpectedConditions.elementToBeClickable()for the login button orExpectedConditions.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()orextract().response()methods are very useful here. - Complex Assertions with Hamcrest: While
equalTois 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:
- Add a
DELETEtest method (testDeleteUser) that runs aftertestUpdateUser. Use thecreatedUserIdto delete the user. Assert that the status code is204 No Content(or200 OKif the mock API responds differently). - Create another POJO
UserResponse.javathat specifically maps the full response for a single user (e.g., when youGET /api/users/2). It might contain adatafield which is aUserobject, and asupportfield which is another object withurlandtext. Deserialise the entire response to thisUserResponsePOJO. - Explore more Hamcrest matchers: In
verifyListOfUsersfromReqResApiTest, add an assertion to check ifdata[1].idis8anddata[1].first_nameisLindsay.
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
Create a
config.propertiesfile: Undersrc/test/resources(create theresourcesfolder if it doesn’t exist), create a new fileconfig.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_HERECreate a utility class to load properties: Create a new package
utilsundersrc/test/java/com/automation/guide. CreatePropertiesLoader.javain theutilspackage.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); } }Update
LoginTestWithPOMto 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."); } } }Update
ReqResApiTestandAdvancedApiTestto use properties forbaseURI:// 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:
- Create a new
config_prod.propertiesfile undersrc/test/resources. Changebase.url.uitohttps://www.google.com(or any other website) andbase.url.apito a different mock API if you know one. - Research Maven Profiles: How would you set up Maven profiles to easily switch between
config.propertiesandconfig_prod.propertieswhen running tests from the command line (e.g.,mvn test -Pprod)? (This is a research challenge, not necessarily code implementation for now). - 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-reportsdirectory, 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):
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.
Create
logback.xml: Undersrc/test/resources, create a new filelogback.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>Integrate logging into your test classes: Modify
LoginTestWithPOM.java(and other test classes) to useLogger.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
LoginPageto useloggerinstead ofSystem.out.printlnfor 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:
- Change the root logger level in
logback.xmltodebug. Rerun tests and observe more detailed output, including internal Selenium and Rest Assured logs. Then change it back toinfo. - Add a
logger.error()call inside acatchblock (e.g., inisFlashMessageDisplayed()ifExpectedConditionsfails) to see how error messages are handled in logs. - 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.
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:
- Expand
getLoginData: Add more test cases, such as an empty username, empty password, or both empty. Observe the application’s behavior and update theexpectedMessagePartaccordingly. - 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). - Data-Driven API Test: Apply the
@DataProviderconcept to one of yourReqResApiTestmethods. For example, create a data provider for different user IDs toGET /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
WebDriverinterface abstracts away browser specifics;Rest Assuredabstracts HTTP calls. - Don’t Repeat Yourself (DRY): Reuse code wherever possible (e.g.,
BaseTestclass 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 likedata-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
@DataProviderto create multiple users with different names and jobs intestCreateUser. For this to work well with chained tests, you’d need to modifyuserIdto 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-driventestCreateUsermethod without chaining them to the sameGET/PUT/DELETEsequence. - 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:
Recommended Online Courses/Tutorials:
- 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:
- Selenium Official Documentation: https://www.selenium.dev/documentation/ - The authoritative source for all things Selenium. Keep an eye on new releases like Selenium 4.35 (August 2025) for the latest features and deprecations.
- Rest Assured Official Documentation: https://rest-assured.io/ and https://github.com/rest-assured/rest-assured/wiki - Your go-to for deep-diving into Rest Assured features.
- TestNG Official Documentation: https://testng.org/doc/index.html - For mastering TestNG’s advanced features.
- Apache Maven Documentation: https://maven.apache.org/guides/ - Essential for advanced build configurations and plugin usage.
- Java Platform, Standard Edition Documentation: https://docs.oracle.com/en/java/javase/ - For deepening your Java knowledge.
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:
- Automation Step by Step: https://www.youtube.com/@AutomationStepByStep/
- ExecuteAutomation: https://www.youtube.com/@ExecuteAutomation
- SDET-QA Automation Techie: https://www.youtube.com/@sdetqaschool
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:
- Test Automation Framework Design: Deep dive into advanced framework architectures (e.g., BDD with Cucumber/Gherkin, Keyword-Driven, Hybrid Frameworks).
- 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.
- Advanced UI Interactions: Handling complex elements like drag-and-drop, rich text editors, file uploads, Shadow DOM.
- Performance Testing: Tools like JMeter or Gatling can be integrated with your API tests.
- Security Testing (API): Tools like OWASP ZAP or Burp Suite to find vulnerabilities in your APIs.
- Mobile Automation (Appium): Extend your UI testing skills to native and hybrid mobile applications using Appium.
- Cloud Testing Platforms: Learn to run your Selenium tests on cloud grids like BrowserStack, Sauce Labs, or LambdaTest for extensive cross-browser/device testing.
- Containerization (Docker/Kubernetes): Use Docker to create consistent, isolated test environments for your automation suite.
- Reporting with ExtentReports or Allure Reports: Create visually appealing and comprehensive test reports.
- 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!