A test fails. The error says "Element not found: #submit-button". Okay... but was the page even loaded? Was there a popup blocking it? Was the page showing an error? Without a screenshot, you are guessing. With a screenshot, you see exactly what the browser showed at the moment of failure. It is the difference between a detective having a witness description and having security camera footage.
Key Point: Every failed test should have a screenshot attached. No exceptions. Automate it once with a listener, and you never have to think about it again.
The quick and easy way. Add screenshot capture in BaseTest so every test class inherits it.
import io.qameta.allure.Allure;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.testng.ITestResult;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import java.io.ByteArrayInputStream;
public class BaseTest {
protected WebDriver driver;
@BeforeMethod
public void setUp() {
driver = DriverFactory.createDriver();
}
@AfterMethod
public void tearDown(ITestResult result) {
if (result.getStatus() == ITestResult.FAILURE && driver != null) {
// Capture screenshot as bytes
byte[] screenshot = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.BYTES);
// Attach to Allure report
Allure.addAttachment(
"Screenshot on Failure",
"image/png",
new ByteArrayInputStream(screenshot),
".png"
);
// Also attach the current URL for context
Allure.addAttachment(
"Page URL",
"text/plain",
driver.getCurrentUrl()
);
}
if (driver != null) {
driver.quit();
}
}
}Always attach the page URL along with the screenshot. The screenshot shows what happened visually. The URL tells you which page the browser was on. Together, they give you the full picture.
A dedicated listener is the better approach for real frameworks. It separates concerns — BaseTest handles setup/teardown, the listener handles reporting. You can add or remove the listener without touching any test class.
package com.practice.listeners;
import io.qameta.allure.Allure;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.logging.LogEntries;
import org.openqa.selenium.logging.LogEntry;
import org.openqa.selenium.logging.LogType;
import org.testng.ITestListener;
import org.testng.ITestResult;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.util.stream.Collectors;
public class AllureScreenshotListener implements ITestListener {
@Override
public void onTestFailure(ITestResult result) {
WebDriver driver = getDriver(result);
if (driver == null) return;
attachScreenshot(driver, "Screenshot on FAILURE");
attachPageUrl(driver);
attachPageSource(driver);
attachBrowserLogs(driver);
}
@Override
public void onTestSkipped(ITestResult result) {
WebDriver driver = getDriver(result);
if (driver == null) return;
attachScreenshot(driver, "Screenshot on SKIP");
}
private void attachScreenshot(WebDriver driver, String name) {
try {
byte[] screenshot = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.BYTES);
Allure.addAttachment(
name, "image/png",
new ByteArrayInputStream(screenshot), ".png"
);
} catch (Exception e) {
Allure.addAttachment("Screenshot Error", "text/plain",
"Failed to capture screenshot: " + e.getMessage());
}
}
private void attachPageUrl(WebDriver driver) {
try {
Allure.addAttachment("Page URL", "text/plain",
driver.getCurrentUrl());
} catch (Exception ignored) {}
}
private void attachPageSource(WebDriver driver) {
try {
Allure.addAttachment("Page Source", "text/html",
driver.getPageSource());
} catch (Exception ignored) {}
}
private void attachBrowserLogs(WebDriver driver) {
try {
LogEntries logs = driver.manage().logs().get(LogType.BROWSER);
if (logs != null && logs.getAll().size() > 0) {
String logText = logs.getAll().stream()
.map(LogEntry::toString)
.collect(Collectors.joining("\n"));
Allure.addAttachment("Browser Console Logs",
"text/plain", logText);
}
} catch (Exception ignored) {
// Some browsers don't support log capture
}
}
/**
* Gets the WebDriver from the test class using reflection.
* Looks for a 'driver' field in the test class or its superclass.
*/
private WebDriver getDriver(ITestResult result) {
try {
Object testInstance = result.getInstance();
Class<?> clazz = testInstance.getClass();
// Look in the class and its superclasses
while (clazz != null) {
try {
Field driverField = clazz.getDeclaredField("driver");
driverField.setAccessible(true);
return (WebDriver) driverField.get(testInstance);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
} catch (Exception e) {
System.err.println("Could not get WebDriver: " + e.getMessage());
}
return null;
}
}<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="Regression Suite">
<listeners>
<listener class-name="com.practice.listeners.AllureScreenshotListener" />
</listeners>
<test name="Banking Tests">
<classes>
<class name="com.practice.tests.banking.LoginTests" />
<class name="com.practice.tests.banking.FundTransferTests" />
</classes>
</test>
<test name="Shopping Tests">
<classes>
<class name="com.practice.tests.shopping.CatalogTests" />
<class name="com.practice.tests.shopping.CheckoutTests" />
</classes>
</test>
</suite>| @AfterMethod Approach | Listener Approach |
|---|---|
| Quick to set up | More code upfront |
| Screenshot logic mixed with teardown | Clean separation of concerns |
| Every test class must extend BaseTest | Works with any test class |
| Hard to add/remove without touching BaseTest | Enable/disable via testng.xml |
| Good for small projects | Best for real frameworks |
The listener uses reflection to access the driver field from BaseTest. If your field is named differently (e.g., "webDriver" instead of "driver"), update the field name in getDriver(). An alternative is to use a ThreadLocal<WebDriver> in a DriverManager class — the listener can then call DriverManager.getDriver() without reflection.
Q: How do you capture screenshots on test failure in your framework?
A: I use a TestNG ITestListener that captures screenshots automatically on test failure. The listener uses reflection to get the WebDriver instance from the test class, takes a screenshot using TakesScreenshot interface, and attaches it to the Allure report using Allure.addAttachment(). I also attach the page URL, page source, and browser console logs for additional debugging context. The listener is registered in testng.xml so it works for all tests without any code changes in test classes. This approach follows the Single Responsibility Principle — the listener handles reporting, BaseTest handles setup/teardown.
Key Point: Automate screenshots on failure with a TestNG listener. Set it up once, debug faster forever.