Advanced Logging with Log4j/SLF4J in Selenium Java
In the world of test automation, effective logging and debugging are crucial components for maintaining robust Selenium Java frameworks. Custom logging not only helps in identifying issues quickly but also provides valuable insights into test execution flow and performance metrics. This comprehensive guide will explore advanced techniques for implementing custom logging in your Selenium Java projects, transforming your test execution from a black box into a transparent and well-documented process.
1. Dependency Setup (Maven)
xml<!-- pom.xml --><dependencies> <!-- Selenium --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.15.0</version> </dependency>
<!-- SLF4J API (Logging Facade) --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.9</version> </dependency>
<!-- Log4j2 Implementation (bound to SLF4J) --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.21.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.21.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j2-impl</artifactId> <version>2.21.1</version> </dependency></dependencies>
xml
<!-- pom.xml -->
<dependencies>
<!-- Selenium -->
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.15.0</version>
</dependency>
<!-- SLF4J API (Logging Facade) -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.9</version>
</dependency>
<!-- Log4j2 Implementation (bound to SLF4J) -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.21.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.21.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.21.1</version>
</dependency>
</dependencies>
2. Log4j2 Configuration (log4j2.xml)
xml<?xml version="1.0" encoding="UTF-8"?><Configuration status="WARN" monitorInterval="30">
<Properties> <Property name="LOG_PATTERN"> %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n </Property> <Property name="BASE_DIR">logs</Property> </Properties>
<Appenders> <!-- Console Appender --> <Console name="ConsoleAppender" target="SYSTEM_OUT"> <PatternLayout pattern="${LOG_PATTERN}" /> <Filters> <ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/> </Filters> </Console>
<!-- File Appender (All Logs) --> <File name="FileAppender" fileName="${BASE_DIR}/automation.log"> <PatternLayout pattern="${LOG_PATTERN}" /> </File>
<!-- Rolling File Appender --> <RollingFile name="RollingFileAppender" fileName="${BASE_DIR}/selenium-rolling.log" filePattern="${BASE_DIR}/archive/selenium-rolling-%d{yyyy-MM-dd}-%i.log.gz"> <PatternLayout pattern="${LOG_PATTERN}" /> <Policies> <TimeBasedTriggeringPolicy interval="1" /> <SizeBasedTriggeringPolicy size="10MB" /> </Policies> <DefaultRolloverStrategy max="30" /> </RollingFile>
<!-- HTML Layout Appender --> <File name="HTMLAppender" fileName="${BASE_DIR}/report.html"> <HTMLLayout charset="UTF-8" title="Selenium Test Logs" /> </File>
<!-- JSON Structured Appender --> <File name="JSONAppender" fileName="${BASE_DIR}/structured.json"> <JsonLayout compact="true" eventEol="true"> <KeyValuePair key="testSuite" value="${ctx:testSuite}" /> <KeyValuePair key="testName" value="${ctx:testName}" /> </JsonLayout> </File>
<!-- Async Appender (Performance) --> <Async name="AsyncAppender"> <AppenderRef ref="RollingFileAppender" /> </Async> </Appenders>
<Loggers> <!-- Selenium-specific logger with custom level --> <Logger name="com.mycompany.automation" level="DEBUG" additivity="false"> <AppenderRef ref="ConsoleAppender" /> <AppenderRef ref="AsyncAppender" /> <AppenderRef ref="JSONAppender" /> </Logger>
<!-- Suppress noisy third-party logs --> <Logger name="org.openqa.selenium" level="WARN" /> <Logger name="org.apache.http" level="WARN" />
<!-- Root Logger --> <Root level="INFO"> <AppenderRef ref="ConsoleAppender" /> <AppenderRef ref="FileAppender" /> </Root> </Loggers></Configuration>
xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Properties>
<Property name="LOG_PATTERN">
%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n
</Property>
<Property name="BASE_DIR">logs</Property>
</Properties>
<Appenders>
<!-- Console Appender -->
<Console name="ConsoleAppender" target="SYSTEM_OUT">
<PatternLayout pattern="${LOG_PATTERN}" />
<Filters>
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
</Filters>
</Console>
<!-- File Appender (All Logs) -->
<File name="FileAppender" fileName="${BASE_DIR}/automation.log">
<PatternLayout pattern="${LOG_PATTERN}" />
</File>
<!-- Rolling File Appender -->
<RollingFile name="RollingFileAppender"
fileName="${BASE_DIR}/selenium-rolling.log"
filePattern="${BASE_DIR}/archive/selenium-rolling-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${LOG_PATTERN}" />
<Policies>
<TimeBasedTriggeringPolicy interval="1" />
<SizeBasedTriggeringPolicy size="10MB" />
</Policies>
<DefaultRolloverStrategy max="30" />
</RollingFile>
<!-- HTML Layout Appender -->
<File name="HTMLAppender" fileName="${BASE_DIR}/report.html">
<HTMLLayout charset="UTF-8" title="Selenium Test Logs" />
</File>
<!-- JSON Structured Appender -->
<File name="JSONAppender" fileName="${BASE_DIR}/structured.json">
<JsonLayout compact="true" eventEol="true">
<KeyValuePair key="testSuite" value="${ctx:testSuite}" />
<KeyValuePair key="testName" value="${ctx:testName}" />
</JsonLayout>
</File>
<!-- Async Appender (Performance) -->
<Async name="AsyncAppender">
<AppenderRef ref="RollingFileAppender" />
</Async>
</Appenders>
<Loggers>
<!-- Selenium-specific logger with custom level -->
<Logger name="com.mycompany.automation" level="DEBUG" additivity="false">
<AppenderRef ref="ConsoleAppender" />
<AppenderRef ref="AsyncAppender" />
<AppenderRef ref="JSONAppender" />
</Logger>
<!-- Suppress noisy third-party logs -->
<Logger name="org.openqa.selenium" level="WARN" />
<Logger name="org.apache.http" level="WARN" />
<!-- Root Logger -->
<Root level="INFO">
<AppenderRef ref="ConsoleAppender" />
<AppenderRef ref="FileAppender" />
</Root>
</Loggers>
</Configuration>
3. Logger Factory & Wrapper
javaimport org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;
public class LogFactory {
private static final Logger logger = LoggerFactory.getLogger(LogFactory.class);
/** * Create a logger for the given class */ public static Logger getLogger(Class<?> clazz) { return LoggerFactory.getLogger(clazz); }
/** * Set MDC context for structured logging */ public static void setTestContext(String testSuite, String testName) { MDC.put("testSuite", testSuite); MDC.put("testName", testName); }
/** * Clear MDC context after test */ public static void clearTestContext() { MDC.clear(); }
// --- Convenience Methods ---
public static void info(String message) { logger.info(message); }
public static void info(String message, Object... args) { logger.info(message, args); }
public static void debug(String message) { logger.debug(message); }
public static void debug(String message, Object... args) { logger.debug(message, args); }
public static void warn(String message) { logger.warn(message); }
public static void warn(String message, Object... args) { logger.warn(message, args); }
public static void error(String message) { logger.error(message); }
public static void error(String message, Throwable t) { logger.error(message, t); }
public static void error(String message, Object... args) { logger.error(message, args); }
public static void step(String stepName) { logger.info(">>> STEP: {}", stepName); }
public static void action(String action, String element) { logger.info("ACTION: {} on [{}]", action, element); }
public static void assertion(String expected, String actual) { logger.info("ASSERT: Expected='{}' | Actual='{}'", expected, actual); }}
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
public class LogFactory {
private static final Logger logger = LoggerFactory.getLogger(LogFactory.class);
/**
* Create a logger for the given class
*/
public static Logger getLogger(Class<?> clazz) {
return LoggerFactory.getLogger(clazz);
}
/**
* Set MDC context for structured logging
*/
public static void setTestContext(String testSuite, String testName) {
MDC.put("testSuite", testSuite);
MDC.put("testName", testName);
}
/**
* Clear MDC context after test
*/
public static void clearTestContext() {
MDC.clear();
}
// --- Convenience Methods ---
public static void info(String message) {
logger.info(message);
}
public static void info(String message, Object... args) {
logger.info(message, args);
}
public static void debug(String message) {
logger.debug(message);
}
public static void debug(String message, Object... args) {
logger.debug(message, args);
}
public static void warn(String message) {
logger.warn(message);
}
public static void warn(String message, Object... args) {
logger.warn(message, args);
}
public static void error(String message) {
logger.error(message);
}
public static void error(String message, Throwable t) {
logger.error(message, t);
}
public static void error(String message, Object... args) {
logger.error(message, args);
}
public static void step(String stepName) {
logger.info(">>> STEP: {}", stepName);
}
public static void action(String action, String element) {
logger.info("ACTION: {} on [{}]", action, element);
}
public static void assertion(String expected, String actual) {
logger.info("ASSERT: Expected='{}' | Actual='{}'", expected, actual);
}
}
4. Custom Selenium WebDriver Listener (Logs Every Action)
javaimport org.openqa.selenium.*;import org.openqa.selenium.support.events.AbstractWebDriverEventListener;import org.slf4j.Logger;import org.slf4j.LoggerFactory;
public class WebDriverLogger extends AbstractWebDriverEventListener {
private static final Logger logger = LoggerFactory.getLogger(WebDriverLogger.class);
@Override public void beforeNavigateTo(String url, WebDriver driver) { logger.info("NAVIGATE TO: {}", url); }
@Override public void afterNavigateBack(WebDriver driver) { logger.info("NAVIGATE BACK"); }
@Override public void beforeFindBy(By by, WebElement element, WebDriver driver) { logger.debug("FINDING ELEMENT: {}", by); }
@Override public void afterFindBy(By by, WebElement element, WebDriver driver) { logger.debug("FOUND ELEMENT: {}", by); }
@Override public void beforeClickOn(WebElement element, WebDriver driver) { logger.info("CLICKING ON: {}", describeElement(element)); }
@Override public void afterClickOn(WebElement element, WebDriver driver) { logger.info("CLICKED ON: {}", describeElement(element)); }
@Override public void beforeChangeValueOf(WebElement element, WebDriver driver, CharSequence[] keysToSend) { logger.info("TYPING '{}' INTO: {}", (keysToSend != null ? String.valueOf(keysToSend) : "null"), describeElement(element)); }
@Override public void afterChangeValueOf(WebElement element, WebDriver driver, CharSequence[] keysToSend) { logger.debug("VALUE CHANGED for: {}", describeElement(element)); }
@Override public void onException(Throwable throwable, WebDriver driver) { logger.error("WEBDRIVER EXCEPTION: {}", throwable.getMessage());
// Capture screenshot on failure if (driver instanceof TakesScreenshot) { try { String screenshot = ((TakesScreenshot) driver) .getScreenshotAs(OutputType.BASE64); logger.error("SCREENSHOT (base64): data:image/png;base64,{}", screenshot.substring(0, Math.min(100, screenshot.length())) + "..."); } catch (Exception e) { logger.warn("Failed to capture screenshot on exception", e); } } }
@Override public void beforeScript(String script, WebDriver driver) { logger.debug("EXECUTING JS: {}", script.length() > 100 ? script.substring(0, 100) + "..." : script); }
@Override public void afterScript(String script, WebDriver driver) { logger.debug("JS EXECUTED SUCCESSFULLY"); }
private String describeElement(WebElement element) { try { return String.format("<%s class='%s' text='%s'>", element.getTagName(), element.getAttribute("class"), element.getText().length() > 30 ? element.getText().substring(0, 30) + "..." : element.getText()); } catch (StaleElementReferenceException e) { return "[STALE ELEMENT]"; } }}
java
import org.openqa.selenium.*;
import org.openqa.selenium.support.events.AbstractWebDriverEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebDriverLogger extends AbstractWebDriverEventListener {
private static final Logger logger = LoggerFactory.getLogger(WebDriverLogger.class);
@Override
public void beforeNavigateTo(String url, WebDriver driver) {
logger.info("NAVIGATE TO: {}", url);
}
@Override
public void afterNavigateBack(WebDriver driver) {
logger.info("NAVIGATE BACK");
}
@Override
public void beforeFindBy(By by, WebElement element, WebDriver driver) {
logger.debug("FINDING ELEMENT: {}", by);
}
@Override
public void afterFindBy(By by, WebElement element, WebDriver driver) {
logger.debug("FOUND ELEMENT: {}", by);
}
@Override
public void beforeClickOn(WebElement element, WebDriver driver) {
logger.info("CLICKING ON: {}", describeElement(element));
}
@Override
public void afterClickOn(WebElement element, WebDriver driver) {
logger.info("CLICKED ON: {}", describeElement(element));
}
@Override
public void beforeChangeValueOf(WebElement element, WebDriver driver,
CharSequence[] keysToSend) {
logger.info("TYPING '{}' INTO: {}",
(keysToSend != null ? String.valueOf(keysToSend) : "null"),
describeElement(element));
}
@Override
public void afterChangeValueOf(WebElement element, WebDriver driver,
CharSequence[] keysToSend) {
logger.debug("VALUE CHANGED for: {}", describeElement(element));
}
@Override
public void onException(Throwable throwable, WebDriver driver) {
logger.error("WEBDRIVER EXCEPTION: {}", throwable.getMessage());
// Capture screenshot on failure
if (driver instanceof TakesScreenshot) {
try {
String screenshot = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.BASE64);
logger.error("SCREENSHOT (base64): data:image/png;base64,{}",
screenshot.substring(0, Math.min(100, screenshot.length())) + "...");
} catch (Exception e) {
logger.warn("Failed to capture screenshot on exception", e);
}
}
}
@Override
public void beforeScript(String script, WebDriver driver) {
logger.debug("EXECUTING JS: {}",
script.length() > 100 ? script.substring(0, 100) + "..." : script);
}
@Override
public void afterScript(String script, WebDriver driver) {
logger.debug("JS EXECUTED SUCCESSFULLY");
}
private String describeElement(WebElement element) {
try {
return String.format("<%s class='%s' text='%s'>",
element.getTagName(),
element.getAttribute("class"),
element.getText().length() > 30
? element.getText().substring(0, 30) + "..."
: element.getText());
} catch (StaleElementReferenceException e) {
return "[STALE ELEMENT]";
}
}
}
5. Selenium 4 Event Listener (Modern Approach)
javaimport org.openqa.selenium.*;import org.openqa.selenium.support.events.WebDriverListener;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.lang.reflect.Method;
public class Selenium4Logger implements WebDriverListener {
private static final Logger logger = LoggerFactory.getLogger(Selenium4Logger.class);
@Override public void beforeAnyWebDriverCall(WebDriver driver, Method method, Object[] args) { logger.debug("WebDriver.{}() called", method.getName()); }
@Override public void afterAnyWebDriverCall(WebDriver driver, Method method, Object[] args, Object result) { logger.debug("WebDriver.{}() completed", method.getName()); }
@Override public void beforeAnyWebElementCall(WebElement element, Method method, Object[] args) { logger.debug("WebElement.{}() called on <{}>", method.getName(), element.getTagName()); }
@Override public void beforeGet(WebDriver driver, String url) { logger.info("NAVIGATE TO: {}", url); }
@Override public void beforeClick(WebElement element) { logger.info("CLICK: <{}> with text='{}'", element.getTagName(), truncate(element.getText())); }
@Override public void beforeSendKeys(WebElement element, CharSequence... keysToSend) { logger.info("SEND KEYS: '{}' to <{}>", maskSensitiveData(String.valueOf(keysToSend)), element.getTagName()); }
@Override public void beforeFindElement(WebDriver driver, By locator) { logger.debug("FIND ELEMENT: {}", locator); }
@Override public void onError(Object target, Method method, Object[] args, InvocationTargetException e) { logger.error("ERROR in {}.{}(): {}", target.getClass().getSimpleName(), method.getName(), e.getTargetException().getMessage()); }
private String truncate(String text) { if (text == null) return "null"; return text.length() > 50 ? text.substring(0, 50) + "..." : text; }
private String maskSensitiveData(String input) { return input.replaceAll("(password|pwd|secret|token)([=:]).*?(?=[&\\s]|$)", "$1$2****"); }}
java
import org.openqa.selenium.*;
import org.openqa.selenium.support.events.WebDriverListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Method;
public class Selenium4Logger implements WebDriverListener {
private static final Logger logger = LoggerFactory.getLogger(Selenium4Logger.class);
@Override
public void beforeAnyWebDriverCall(WebDriver driver, Method method, Object[] args) {
logger.debug("WebDriver.{}() called", method.getName());
}
@Override
public void afterAnyWebDriverCall(WebDriver driver, Method method,
Object[] args, Object result) {
logger.debug("WebDriver.{}() completed", method.getName());
}
@Override
public void beforeAnyWebElementCall(WebElement element, Method method, Object[] args) {
logger.debug("WebElement.{}() called on <{}>", method.getName(),
element.getTagName());
}
@Override
public void beforeGet(WebDriver driver, String url) {
logger.info("NAVIGATE TO: {}", url);
}
@Override
public void beforeClick(WebElement element) {
logger.info("CLICK: <{}> with text='{}'", element.getTagName(),
truncate(element.getText()));
}
@Override
public void beforeSendKeys(WebElement element, CharSequence... keysToSend) {
logger.info("SEND KEYS: '{}' to <{}>",
maskSensitiveData(String.valueOf(keysToSend)), element.getTagName());
}
@Override
public void beforeFindElement(WebDriver driver, By locator) {
logger.debug("FIND ELEMENT: {}", locator);
}
@Override
public void onError(Object target, Method method, Object[] args,
InvocationTargetException e) {
logger.error("ERROR in {}.{}(): {}",
target.getClass().getSimpleName(),
method.getName(),
e.getTargetException().getMessage());
}
private String truncate(String text) {
if (text == null) return "null";
return text.length() > 50 ? text.substring(0, 50) + "..." : text;
}
private String maskSensitiveData(String input) {
return input.replaceAll("(password|pwd|secret|token)([=:]).*?(?=[&\\s]|$)",
"$1$2****");
}
}
6. Logging Base Test Class
javaimport org.openqa.selenium.WebDriver;import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.chrome.ChromeOptions;import org.openqa.selenium.support.events.EventFiringDecorator;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.testng.annotations.*;
import java.lang.reflect.Method;import java.time.Duration;
public class BaseTest {
protected WebDriver driver; protected Logger log;
@Parameters("browser") @BeforeClass public void setUpClass(@Optional("chrome") String browser) { LogFactory.setTestContext( this.getClass().getSimpleName(), "class-setup"); log = LoggerFactory.getLogger(this.getClass()); log.info("========== Starting Test Class: {} ==========", this.getClass().getSimpleName()); }
@BeforeMethod public void setUp(Method method) { LogFactory.setTestContext( this.getClass().getSimpleName(), method.getName()); log = LoggerFactory.getLogger(this.getClass()); log.info("----- Starting Test: {} -----", method.getName());
// Create base driver WebDriver baseDriver = createDriver("chrome");
// Wrap with logging listener (Selenium 4) Selenium4Logger listener = new Selenium4Logger(); this.driver = new EventFiringDecorator<>(WebDriver.class, listener) .decorate(baseDriver);
this.driver.manage().timeouts() .implicitlyWait(Duration.ofSeconds(10)); this.driver.manage().window().maximize(); }
@AfterMethod public void tearDown(Method method) { if (driver != null) { log.info("----- Finished Test: {} -----", method.getName()); driver.quit(); } LogFactory.clearTestContext(); }
@AfterClass public void tearDownClass() { log.info("========== Finished Test Class: {} ==========", this.getClass().getSimpleName()); }
private WebDriver createDriver(String browser) { ChromeOptions options = new ChromeOptions(); options.addArguments("--headless=new"); options.addArguments("--disable-gpu"); return new ChromeDriver(options); }}
java
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.events.EventFiringDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.testng.annotations.*;
import java.lang.reflect.Method;
import java.time.Duration;
public class BaseTest {
protected WebDriver driver;
protected Logger log;
@Parameters("browser")
@BeforeClass
public void setUpClass(@Optional("chrome") String browser) {
LogFactory.setTestContext(
this.getClass().getSimpleName(), "class-setup");
log = LoggerFactory.getLogger(this.getClass());
log.info("========== Starting Test Class: {} ==========",
this.getClass().getSimpleName());
}
@BeforeMethod
public void setUp(Method method) {
LogFactory.setTestContext(
this.getClass().getSimpleName(), method.getName());
log = LoggerFactory.getLogger(this.getClass());
log.info("----- Starting Test: {} -----", method.getName());
// Create base driver
WebDriver baseDriver = createDriver("chrome");
// Wrap with logging listener (Selenium 4)
Selenium4Logger listener = new Selenium4Logger();
this.driver = new EventFiringDecorator<>(WebDriver.class, listener)
.decorate(baseDriver);
this.driver.manage().timeouts()
.implicitlyWait(Duration.ofSeconds(10));
this.driver.manage().window().maximize();
}
@AfterMethod
public void tearDown(Method method) {
if (driver != null) {
log.info("----- Finished Test: {} -----", method.getName());
driver.quit();
}
LogFactory.clearTestContext();
}
@AfterClass
public void tearDownClass() {
log.info("========== Finished Test Class: {} ==========",
this.getClass().getSimpleName());
}
private WebDriver createDriver(String browser) {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless=new");
options.addArguments("--disable-gpu");
return new ChromeDriver(options);
}
}
7. Page Object with Structured Logging
javaimport org.openqa.selenium.*;import org.openqa.selenium.support.*;import org.openqa.selenium.support.ui.*;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.time.Duration;
public class LoginPage {
private final WebDriver driver; private final WebDriverWait wait; private final Logger log = LoggerFactory.getLogger(LoginPage.class);
@FindBy(id = "username") private WebElement usernameField;
@FindBy(id = "password") private WebElement passwordField;
@FindBy(css = "button[type='submit']") private WebElement loginButton;
@FindBy(css = ".error-message") private WebElement errorMessage;
public LoginPage(WebDriver driver) { this.driver = driver; this.wait = new WebDriverWait(driver, Duration.ofSeconds(10)); PageFactory.initElements(driver, this); }
public DashboardPage loginAs(String username, String password) { log.info("Login attempt for user: '{}'", username); log.debug("Entering username..."); enterUsername(username);
log.debug("Entering password..."); enterPassword(password);
log.debug("Clicking login button..."); clickLogin();
log.info("Login completed for user: '{}'", username); return new DashboardPage(driver); }
public LoginPage loginExpectingFailure(String username, String password) { log.warn("Login attempt expected to fail for user: '{}'", username); enterUsername(username); enterPassword(password); clickLogin(); return this; }
public String getErrorMessage() { wait.until(ExpectedConditions.visibilityOf(errorMessage)); String error = errorMessage.getText(); log.error("Error displayed on login page: '{}'", error); return error; }
private void enterUsername(String username) { try { wait.until(ExpectedConditions.visibilityOf(usernameField)); usernameField.clear(); usernameField.sendKeys(username); log.debug("Username entered successfully"); } catch (TimeoutException e) { log.error("Username field not found within timeout"); throw e; } }
private void enterPassword(String password) { try { wait.until(ExpectedConditions.visibilityOf(passwordField)); passwordField.clear(); passwordField.sendKeys(password); log.debug("Password entered successfully"); } catch (TimeoutException e) { log.error("Password field not found within timeout"); throw e; } }
private void clickLogin() { try { wait.until(ExpectedConditions.elementToBeClickable(loginButton)); loginButton.click(); log.debug("Login button clicked"); } catch (TimeoutException e) { log.error("Login button not clickable within timeout"); throw e; } }}
java
import org.openqa.selenium.*;
import org.openqa.selenium.support.*;
import org.openqa.selenium.support.ui.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
public class LoginPage {
private final WebDriver driver;
private final WebDriverWait wait;
private final Logger log = LoggerFactory.getLogger(LoginPage.class);
@FindBy(id = "username")
private WebElement usernameField;
@FindBy(id = "password")
private WebElement passwordField;
@FindBy(css = "button[type='submit']")
private WebElement loginButton;
@FindBy(css = ".error-message")
private WebElement errorMessage;
public LoginPage(WebDriver driver) {
this.driver = driver;
this.wait = new WebDriverWait(driver, Duration.ofSeconds(10));
PageFactory.initElements(driver, this);
}
public DashboardPage loginAs(String username, String password) {
log.info("Login attempt for user: '{}'", username);
log.debug("Entering username...");
enterUsername(username);
log.debug("Entering password...");
enterPassword(password);
log.debug("Clicking login button...");
clickLogin();
log.info("Login completed for user: '{}'", username);
return new DashboardPage(driver);
}
public LoginPage loginExpectingFailure(String username, String password) {
log.warn("Login attempt expected to fail for user: '{}'", username);
enterUsername(username);
enterPassword(password);
clickLogin();
return this;
}
public String getErrorMessage() {
wait.until(ExpectedConditions.visibilityOf(errorMessage));
String error = errorMessage.getText();
log.error("Error displayed on login page: '{}'", error);
return error;
}
private void enterUsername(String username) {
try {
wait.until(ExpectedConditions.visibilityOf(usernameField));
usernameField.clear();
usernameField.sendKeys(username);
log.debug("Username entered successfully");
} catch (TimeoutException e) {
log.error("Username field not found within timeout");
throw e;
}
}
private void enterPassword(String password) {
try {
wait.until(ExpectedConditions.visibilityOf(passwordField));
passwordField.clear();
passwordField.sendKeys(password);
log.debug("Password entered successfully");
} catch (TimeoutException e) {
log.error("Password field not found within timeout");
throw e;
}
}
private void clickLogin() {
try {
wait.until(ExpectedConditions.elementToBeClickable(loginButton));
loginButton.click();
log.debug("Login button clicked");
} catch (TimeoutException e) {
log.error("Login button not clickable within timeout");
throw e;
}
}
}
8. Screenshot Logger Utility
javaimport org.openqa.selenium.*;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.io.File;import java.io.IOException;import java.nio.file.*;import java.time.LocalDateTime;import java.time.format.DateTimeFormatter;
public class ScreenshotLogger {
private static final Logger logger = LoggerFactory.getLogger(ScreenshotLogger.class); private static final String SCREENSHOT_DIR = "logs/screenshots"; private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss_SSS");
/** * Capture screenshot and log the file path */ public static String capture(WebDriver driver, String testName) { try { createScreenshotDir(); String timestamp = LocalDateTime.now().format(formatter); String fileName = String.format("%s_%s.png", testName, timestamp); Path filePath = Paths.get(SCREENSHOT_DIR, fileName);
File screenshot = ((TakesScreenshot) driver) .getScreenshotAs(OutputType.FILE); Files.copy(screenshot.toPath(), filePath, StandardCopyOption.REPLACE_EXISTING);
logger.info("SCREENSHOT SAVED: {}", filePath.toAbsolutePath()); return filePath.toString();
} catch (IOException e) { logger.error("Failed to save screenshot: {}", e.getMessage()); return null; } catch (Exception e) { logger.error("Failed to capture screenshot: {}", e.getMessage()); return null; } }
/** * Capture screenshot with base64 (for embedding in reports) */ public static String captureBase64(WebDriver driver) { try { String base64 = ((TakesScreenshot) driver) .getScreenshotAs(OutputType.BASE64); logger.debug("Base64 screenshot captured ({} chars)", base64.length()); return base64; } catch (Exception e) { logger.error("Failed to capture base64 screenshot: {}", e.getMessage()); return null; } }
/** * Capture element-specific screenshot */ public static String captureElement(WebElement element, String elementName) { try { createScreenshotDir(); String timestamp = LocalDateTime.now().format(formatter); String fileName = String.format("element_%s_%s.png", elementName, timestamp); Path filePath = Paths.get(SCREENSHOT_DIR, fileName);
File screenshot = element.getScreenshotAs(OutputType.FILE); Files.copy(screenshot.toPath(), filePath, StandardCopyOption.REPLACE_EXISTING);
logger.info("ELEMENT SCREENSHOT SAVED: {}", filePath.toAbsolutePath()); return filePath.toString();
} catch (Exception e) { logger.error("Failed to capture element screenshot: {}", e.getMessage()); return null; } }
private static void createScreenshotDir() throws IOException { Path dir = Paths.get(SCREENSHOT_DIR); if (!Files.exists(dir)) { Files.createDirectories(dir); } }}
java
import org.openqa.selenium.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class ScreenshotLogger {
private static final Logger logger = LoggerFactory.getLogger(ScreenshotLogger.class);
private static final String SCREENSHOT_DIR = "logs/screenshots";
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss_SSS");
/**
* Capture screenshot and log the file path
*/
public static String capture(WebDriver driver, String testName) {
try {
createScreenshotDir();
String timestamp = LocalDateTime.now().format(formatter);
String fileName = String.format("%s_%s.png", testName, timestamp);
Path filePath = Paths.get(SCREENSHOT_DIR, fileName);
File screenshot = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.FILE);
Files.copy(screenshot.toPath(), filePath,
StandardCopyOption.REPLACE_EXISTING);
logger.info("SCREENSHOT SAVED: {}", filePath.toAbsolutePath());
return filePath.toString();
} catch (IOException e) {
logger.error("Failed to save screenshot: {}", e.getMessage());
return null;
} catch (Exception e) {
logger.error("Failed to capture screenshot: {}", e.getMessage());
return null;
}
}
/**
* Capture screenshot with base64 (for embedding in reports)
*/
public static String captureBase64(WebDriver driver) {
try {
String base64 = ((TakesScreenshot) driver)
.getScreenshotAs(OutputType.BASE64);
logger.debug("Base64 screenshot captured ({} chars)", base64.length());
return base64;
} catch (Exception e) {
logger.error("Failed to capture base64 screenshot: {}", e.getMessage());
return null;
}
}
/**
* Capture element-specific screenshot
*/
public static String captureElement(WebElement element, String elementName) {
try {
createScreenshotDir();
String timestamp = LocalDateTime.now().format(formatter);
String fileName = String.format("element_%s_%s.png",
elementName, timestamp);
Path filePath = Paths.get(SCREENSHOT_DIR, fileName);
File screenshot = element.getScreenshotAs(OutputType.FILE);
Files.copy(screenshot.toPath(), filePath,
StandardCopyOption.REPLACE_EXISTING);
logger.info("ELEMENT SCREENSHOT SAVED: {}", filePath.toAbsolutePath());
return filePath.toString();
} catch (Exception e) {
logger.error("Failed to capture element screenshot: {}", e.getMessage());
return null;
}
}
private static void createScreenshotDir() throws IOException {
Path dir = Paths.get(SCREENSHOT_DIR);
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
}
}
9. TestNG Listener with Logging Integration
javaimport org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.slf4j.MDC;import org.testng.*;import org.openqa.selenium.WebDriver;
public class LoggingTestListener implements ITestListener, ISuiteListener {
private static final Logger logger = LoggerFactory.getLogger(LoggingTestListener.class);
@Override public void onStart(ISuite suite) { logger.info("╔══════════════════════════════════════════╗"); logger.info("║ SUITE STARTED: {} ", suite.getName()); logger.info("╚══════════════════════════════════════════╝"); }
@Override public void onTestStart(ITestResult result) { MDC.put("testName", result.getMethod().getMethodName()); MDC.put("testClass", result.getTestClass().getName());
logger.info("┌─────────────────────────────────────┐"); logger.info("│ TEST STARTED: {}.{}", result.getTestClass().getSimpleName(), result.getMethod().getMethodName()); logger.info("└─────────────────────────────────────┘"); }
@Override public void onTestSuccess(ITestResult result) { long duration = result.getEndMillis() - result.getStartMillis(); logger.info("✅ TEST PASSED: {}.{} ({}ms)", result.getTestClass().getSimpleName(), result.getMethod().getMethodName(), duration); MDC.remove("testName"); }
@Override public void onTestFailure(ITestResult result) { long duration = result.getEndMillis() - result.getStartMillis(); logger.error("❌ TEST FAILED: {}.{} ({}ms)", result.getTestClass().getSimpleName(), result.getMethod().getMethodName(), duration);
if (result.getThrowable() != null) { logger.error("Exception: ", result.getThrowable()); }
// Capture screenshot from test instance Object testInstance = result.getInstance(); if (testInstance instanceof BaseTest) { WebDriver driver = ((BaseTest) testInstance).driver; if (driver != null) { ScreenshotLogger.capture(driver, result.getMethod().getMethodName()); } }
MDC.remove("testName"); }
@Override public void onTestSkipped(ITestResult result) { logger.warn("⚠️ TEST SKIPPED: {}.{}", result.getTestClass().getSimpleName(), result.getMethod().getMethodName()); MDC.remove("testName"); }
@Override public void onFinish(ISuite suite) { logger.info("╔══════════════════════════════════════════╗"); logger.info("║ SUITE FINISHED: {} ", suite.getName()); logger.info("╚══════════════════════════════════════════╝"); }}
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.testng.*;
import org.openqa.selenium.WebDriver;
public class LoggingTestListener implements ITestListener, ISuiteListener {
private static final Logger logger = LoggerFactory.getLogger(LoggingTestListener.class);
@Override
public void onStart(ISuite suite) {
logger.info("╔══════════════════════════════════════════╗");
logger.info("║ SUITE STARTED: {} ", suite.getName());
logger.info("╚══════════════════════════════════════════╝");
}
@Override
public void onTestStart(ITestResult result) {
MDC.put("testName", result.getMethod().getMethodName());
MDC.put("testClass", result.getTestClass().getName());
logger.info("┌─────────────────────────────────────┐");
logger.info("│ TEST STARTED: {}.{}",
result.getTestClass().getSimpleName(),
result.getMethod().getMethodName());
logger.info("└─────────────────────────────────────┘");
}
@Override
public void onTestSuccess(ITestResult result) {
long duration = result.getEndMillis() - result.getStartMillis();
logger.info("✅ TEST PASSED: {}.{} ({}ms)",
result.getTestClass().getSimpleName(),
result.getMethod().getMethodName(),
duration);
MDC.remove("testName");
}
@Override
public void onTestFailure(ITestResult result) {
long duration = result.getEndMillis() - result.getStartMillis();
logger.error("❌ TEST FAILED: {}.{} ({}ms)",
result.getTestClass().getSimpleName(),
result.getMethod().getMethodName(),
duration);
if (result.getThrowable() != null) {
logger.error("Exception: ", result.getThrowable());
}
// Capture screenshot from test instance
Object testInstance = result.getInstance();
if (testInstance instanceof BaseTest) {
WebDriver driver = ((BaseTest) testInstance).driver;
if (driver != null) {
ScreenshotLogger.capture(driver, result.getMethod().getMethodName());
}
}
MDC.remove("testName");
}
@Override
public void onTestSkipped(ITestResult result) {
logger.warn("⚠️ TEST SKIPPED: {}.{}",
result.getTestClass().getSimpleName(),
result.getMethod().getMethodName());
MDC.remove("testName");
}
@Override
public void onFinish(ISuite suite) {
logger.info("╔══════════════════════════════════════════╗");
logger.info("║ SUITE FINISHED: {} ", suite.getName());
logger.info("╚══════════════════════════════════════════╝");
}
}
10. Retry Analyzer with Logging
javaimport org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.testng.IRetryAnalyzer;import org.testng.ITestResult;
public class LoggingRetryAnalyzer implements IRetryAnalyzer {
private static final Logger logger = LoggerFactory.getLogger(LoggingRetryAnalyzer.class); private int retryCount = 0; private static final int MAX_RETRIES = 2;
@Override public boolean retry(ITestResult result) { if (retryCount < MAX_RETRIES) { retryCount++; logger.warn("🔄 RETRYING test: {}.{} (attempt {}/{})", result.getTestClass().getSimpleName(), result.getMethod().getMethodName(), retryCount + 1, MAX_RETRIES + 1); return true; }
logger.error("❌ MAX RETRIES ({}) exceeded for: {}.{}", MAX_RETRIES, result.getTestClass().getSimpleName(), result.getMethod().getMethodName()); return false; }}
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.IRetryAnalyzer;
import org.testng.ITestResult;
public class LoggingRetryAnalyzer implements IRetryAnalyzer {
private static final Logger logger = LoggerFactory.getLogger(LoggingRetryAnalyzer.class);
private int retryCount = 0;
private static final int MAX_RETRIES = 2;
@Override
public boolean retry(ITestResult result) {
if (retryCount < MAX_RETRIES) {
retryCount++;
logger.warn("🔄 RETRYING test: {}.{} (attempt {}/{})",
result.getTestClass().getSimpleName(),
result.getMethod().getMethodName(),
retryCount + 1,
MAX_RETRIES + 1);
return true;
}
logger.error("❌ MAX RETRIES ({}) exceeded for: {}.{}",
MAX_RETRIES,
result.getTestClass().getSimpleName(),
result.getMethod().getMethodName());
return false;
}
}
11. TestNG XML Configuration
xml<!-- testng.xml --><!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd"><suite name="AutomationSuite" verbose="1"> <listeners> <listener class-name="com.mycompany.automation.listeners.LoggingTestListener" /> </listeners> <test name="RegressionTests"> <classes> <class name="com.mycompany.automation.tests.LoginTests" /> <class name="com.mycompany.automation.tests.SearchTests" /> </classes> </test></suite>
xml
<!-- testng.xml -->
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="AutomationSuite" verbose="1">
<listeners>
<listener class-name="com.mycompany.automation.listeners.LoggingTestListener" />
</listeners>
<test name="RegressionTests">
<classes>
<class name="com.mycompany.automation.tests.LoginTests" />
<class name="com.mycompany.automation.tests.SearchTests" />
</classes>
</test>
</suite>
12. Sample Test Class
javaimport org.testng.annotations.Test;import org.testng.Assert;
public class LoginTests extends BaseTest {
@Test(retryAnalyzer = LoggingRetryAnalyzer.class) public void testValidLogin() { LogFactory.step("Navigate to login page"); driver.get("https://example.com/login");
LogFactory.step("Perform login"); LoginPage loginPage = new LoginPage(driver); DashboardPage dashboard = loginPage.loginAs("admin", "password123");
LogFactory.step("Verify dashboard is displayed"); Assert.assertTrue(dashboard.isDisplayed(), "Dashboard should be visible after login"); LogFactory.info("Login test completed successfully"); }
@Test public void testInvalidLogin() { LogFactory.step("Navigate to login page"); driver.get("https://example.com/login");
LogFactory.step("Attempt login with invalid credentials"); LoginPage loginPage = new LoginPage(driver); loginPage.loginExpectingFailure("admin", "wrongpassword");
LogFactory.step("Verify error message"); String error = loginPage.getErrorMessage(); LogFactory.assertion("Invalid credentials", error); Assert.assertTrue(error.contains("Invalid"), "Error message should indicate invalid credentials"); }}
java
import org.testng.annotations.Test;
import org.testng.Assert;
public class LoginTests extends BaseTest {
@Test(retryAnalyzer = LoggingRetryAnalyzer.class)
public void testValidLogin() {
LogFactory.step("Navigate to login page");
driver.get("https://example.com/login");
LogFactory.step("Perform login");
LoginPage loginPage = new LoginPage(driver);
DashboardPage dashboard = loginPage.loginAs("admin", "password123");
LogFactory.step("Verify dashboard is displayed");
Assert.assertTrue(dashboard.isDisplayed(),
"Dashboard should be visible after login");
LogFactory.info("Login test completed successfully");
}
@Test
public void testInvalidLogin() {
LogFactory.step("Navigate to login page");
driver.get("https://example.com/login");
LogFactory.step("Attempt login with invalid credentials");
LoginPage loginPage = new LoginPage(driver);
loginPage.loginExpectingFailure("admin", "wrongpassword");
LogFactory.step("Verify error message");
String error = loginPage.getErrorMessage();
LogFactory.assertion("Invalid credentials", error);
Assert.assertTrue(error.contains("Invalid"),
"Error message should indicate invalid credentials");
}
}
13. Log Level Hierarchy & When to Use Each
text┌──────────────────────────────────────────────────────────────┐│ LEVEL │ USE CASE │├───────────┼──────────────────────────────────────────────────┤│ TRACE │ Selenium wire-level communication, raw HTML ││ DEBUG │ Element locators, wait conditions, DOM state ││ INFO │ Test steps, navigation, actions, assertions ││ WARN │ Retries, slow operations, deprecated features ││ ERROR │ Test failures, exceptions, screenshot capture ││ FATAL │ Environment setup failures, critical errors │└──────────────────────────────────────────────────────────────┘
text
┌──────────────────────────────────────────────────────────────┐
│ LEVEL │ USE CASE │
├───────────┼──────────────────────────────────────────────────┤
│ TRACE │ Selenium wire-level communication, raw HTML │
│ DEBUG │ Element locators, wait conditions, DOM state │
│ INFO │ Test steps, navigation, actions, assertions │
│ WARN │ Retries, slow operations, deprecated features │
│ ERROR │ Test failures, exceptions, screenshot capture │
│ FATAL │ Environment setup failures, critical errors │
└──────────────────────────────────────────────────────────────┘
14. Best Practices Summary
text✅ DO: • Use SLF4J as facade → Log4j2 as implementation (swappable) • Use parameterized logging: log.info("User: {}", username) • Use MDC for test context (suite, test name, thread) • Use Async appenders for performance in parallel tests • Capture screenshots on failure at listener level • Log BEFORE actions (not just after) for debugging hangs • Use rolling file appender to prevent disk overflow • Suppress noisy third-party loggers (selenium, http, etc.) • Use WebDriver Event Listeners for automatic action logging
❌ DON'T: • Use System.out.println() — use logger instead • Log sensitive data (passwords, tokens, PII) — mask it • Log entire page sources at INFO level — use TRACE/DEBUG • String concatenate in log statements — use {} placeholders • Create logger instances per method — one per class (static final) • Forget to clear MDC after each test — causes thread bleed
text
✅ DO:
• Use SLF4J as facade → Log4j2 as implementation (swappable)
• Use parameterized logging: log.info("User: {}", username)
• Use MDC for test context (suite, test name, thread)
• Use Async appenders for performance in parallel tests
• Capture screenshots on failure at listener level
• Log BEFORE actions (not just after) for debugging hangs
• Use rolling file appender to prevent disk overflow
• Suppress noisy third-party loggers (selenium, http, etc.)
• Use WebDriver Event Listeners for automatic action logging
❌ DON'T:
• Use System.out.println() — use logger instead
• Log sensitive data (passwords, tokens, PII) — mask it
• Log entire page sources at INFO level — use TRACE/DEBUG
• String concatenate in log statements — use {} placeholders
• Create logger instances per method — one per class (static final)
• Forget to clear MDC after each test — causes thread bleed
15. Sample Log Output
text2024-01-15 10:30:45.123 [main] INFO c.m.a.tests.LoginTests - ----- Starting Test: testValidLogin -----2024-01-15 10:30:45.456 [main] INFO c.m.a.pages.LoginPage - Login attempt for user: 'admin'2024-01-15 10:30:45.789 [main] DEBUG c.m.a.pages.LoginPage - Entering username...2024-01-15 10:30:46.012 [main] DEBUG c.m.a.pages.LoginPage - Username entered successfully2024-01-15 10:30:46.123 [main] INFO c.m.a.listeners.Selenium4Logger - SEND KEYS: '****' to <input>2024-01-15 10:30:46.456 [main] INFO c.m.a.listeners.Selenium4Logger - CLICK: <button> with text='Sign In'2024-01-15 10:30:47.789 [main] INFO c.m.a.pages.LoginPage - Login completed for user: 'admin'2024-01-15 10:30:48.012 [main] INFO c.m.a.tests.LoginTests - ✅ TEST PASSED: LoginTests.testValidLogin (2889ms)
text
2024-01-15 10:30:45.123 [main] INFO c.m.a.tests.LoginTests - ----- Starting Test: testValidLogin -----
2024-01-15 10:30:45.456 [main] INFO c.m.a.pages.LoginPage - Login attempt for user: 'admin'
2024-01-15 10:30:45.789 [main] DEBUG c.m.a.pages.LoginPage - Entering username...
2024-01-15 10:30:46.012 [main] DEBUG c.m.a.pages.LoginPage - Username entered successfully
2024-01-15 10:30:46.123 [main] INFO c.m.a.listeners.Selenium4Logger - SEND KEYS: '****' to <input>
2024-01-15 10:30:46.456 [main] INFO c.m.a.listeners.Selenium4Logger - CLICK: <button> with text='Sign In'
2024-01-15 10:30:47.789 [main] INFO c.m.a.pages.LoginPage - Login completed for user: 'admin'
2024-01-15 10:30:48.012 [main] INFO c.m.a.tests.LoginTests - ✅ TEST PASSED: LoginTests.testValidLogin (2889ms)
This architecture provides comprehensive, structured, and performant logging that covers every layer of a Selenium test automation framework—from individual element interactions to suite-level reportin
Frequently Asked Questions
- Why is logging important in Selenium test automation?
Logging helps track test execution flow, identify failures, and debug issues efficiently in automated tests. - What is the difference between Log4j and SLF4J?
Log4j is a logging implementation framework, while SLF4J is a logging abstraction layer that allows switching between different logging implementations. - How do I integrate Log4j with Selenium Java tests?
Add Log4j dependencies to your project, create a configuration file, and use Logger instances in your test code to capture detailed execution logs. - What are best practices for logging in Selenium tests?
Use appropriate log levels, include meaningful messages, avoid sensitive data in logs, and structure logs for easy analysis. - Can I use multiple logging frameworks in Selenium projects?
Yes, SLF4J allows binding to different logging implementations, enabling you to switch or combine logging frameworks as needed.
No comments:
Post a Comment