A failed test with no visual evidence wastes debugging time. Screenshots taken at the moment of failure tell you exactly what state the app was in. Screen recordings tell you how it got there.
Screenshot on failure with ITestListener
import org.testng.ITestListener;
import org.testng.ITestResult;
import org.openqa.selenium.OutputType;
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ScreenshotListener implements ITestListener {
@Override
public void onTestFailure(ITestResult result) {
AppiumDriver driver = DriverManager.getDriver();
if (driver == null) return;
String timestamp = LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")
);
String testName = result.getName();
String fileName = String.format("target/screenshots/%s_%s.png", testName, timestamp);
try {
File srcFile = driver.getScreenshotAs(OutputType.FILE);
FileUtils.copyFile(srcFile, new File(fileName));
System.out.println("Screenshot saved: " + fileName);
} catch (IOException e) {
System.err.println("Failed to save screenshot: " + e.getMessage());
}
}
}Register in testng.xml:
<suite name="Mobile Suite">
<listeners>
<listener class-name="com.example.listeners.ScreenshotListener"/>
<listener class-name="com.example.listeners.RetryTransformer"/>
</listeners>
<!-- ... -->
</suite>Ensure target/screenshots/ exists before tests run (Maven creates target/ automatically; the subdirectory may need creation):
@Override
public void onTestFailure(ITestResult result) {
new File("target/screenshots").mkdirs();
// ... rest of method
}Attaching screenshots to Allure reports
If you use Allure, attach the screenshot directly to the report rather than saving to a file:
import io.qameta.allure.Allure;
import java.io.ByteArrayInputStream;
@Override
public void onTestFailure(ITestResult result) {
AppiumDriver driver = DriverManager.getDriver();
if (driver == null) return;
byte[] screenshot = driver.getScreenshotAs(OutputType.BYTES);
Allure.addAttachment(
"Screenshot on failure",
"image/png",
new ByteArrayInputStream(screenshot),
"png"
);
}Allure embeds the image in the HTML report — no separate screenshots folder needed.
Screen recording on failure
Screen recordings provide the "how did we get here" context that screenshots can't. Start recording at @BeforeMethod and stop at @AfterMethod, saving only on failure:
public class RecordingListener implements ITestListener {
@Override
public void onTestStart(ITestResult result) {
AppiumDriver driver = DriverManager.getDriver();
if (driver == null) return;
driver.startRecordingScreen(
new AndroidStartScreenRecordingOptions()
.withTimeLimit(Duration.ofMinutes(5))
.withVideoSize("1080x1920")
.withBitRate(2_000_000)
);
}
@Override
public void onTestFailure(ITestResult result) {
saveRecording(result.getName() + "_FAILED");
}
@Override
public void onTestSuccess(ITestResult result) {
// Stop recording but discard — only keep failures
AppiumDriver driver = DriverManager.getDriver();
if (driver != null) {
driver.stopRecordingScreen(); // discard returned base64
}
}
private void saveRecording(String fileName) {
AppiumDriver driver = DriverManager.getDriver();
if (driver == null) return;
String base64 = driver.stopRecordingScreen();
byte[] videoBytes = Base64.getDecoder().decode(base64);
new File("target/recordings").mkdirs();
try {
Files.write(
Paths.get("target/recordings/" + fileName + ".mp4"),
videoBytes
);
} catch (IOException e) {
System.err.println("Failed to save recording: " + e.getMessage());
}
}
}iOS recording uses IOSStartScreenRecordingOptions:
driver.startRecordingScreen(
new IOSStartScreenRecordingOptions()
.withTimeLimit(Duration.ofMinutes(5))
.withVideoType("mp4")
.withVideoFps("30")
);Page source on failure
The view hierarchy is sometimes more useful than a screenshot for debugging locator failures. Capture it alongside the screenshot:
@Override
public void onTestFailure(ITestResult result) {
AppiumDriver driver = DriverManager.getDriver();
if (driver == null) return;
// Screenshot
// ...
// Page source
String pageSource = driver.getPageSource();
String testName = result.getName();
try {
Files.writeString(
Paths.get("target/screenshots/" + testName + "_hierarchy.xml"),
pageSource
);
} catch (IOException e) {
System.err.println("Failed to save page source: " + e.getMessage());
}
}The XML file is the same content you'd see in Appium Inspector — searchable for element IDs and attributes.
Handling screenshots during parallel runs
With parallel tests, multiple threads may call onTestFailure simultaneously. The DriverManager.getDriver() call is thread-safe because it reads from ThreadLocal. The file write should use a unique filename per test — include thread ID if needed:
String fileName = String.format("target/screenshots/%s_%d_%s.png",
testName, Thread.currentThread().getId(), timestamp);This prevents two simultaneously failing tests from overwriting each other's screenshots.