Log each step with text and screenshot in Selenium Java.

Nearly all Selenium test automation engineers sooner or later run into the logging issue. Some tend to think out logging capabilities of their frameworks or solutions at the design phase which is really good. Some have to use old legacy frameworks with thousand of code lines. For the latter ones the task of covering all the code with logs might really kill their passion of being developers in test.

Fortunately Selenium provides some utility that allow to cover even legacy code with code lines. Actually it makes us able to bind any activity to almost all the actions that WebDriver or WebElement provide. In this article we model the "legacy framework" and will add some logging feature to it.

Example description: Text and screenshots logging for each Selenium step.

We’re going to model a legacy testing system. That model will consist of a single class but we’ll pretend that it has much more test methods and much more classes which probably extend this one demonstrated, and they have some single point of obtaining the driver they’re going to work with. Our task is to add logging of what we’re doing in our tests not introducing the changes to tests code at all. Here is what we have as precondition.

package click.webelement.logging;

import org.junit.jupiter.api.*;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class LegacyTest {

    WebDriver driver;

    @BeforeAll
    public static void globalSetup(){
        System.setProperty("webdriver.gecko.driver", "/home/alexey/Desktop/Dev/webdrivers/geckodriver");
    }

    @BeforeEach
    public void setUp(){
        driver = new FirefoxDriver();
    }

    @Test
    @DisplayName("WebElement.Click demo legacy tests")
    public void testLogging(){
        driver.get("https://webelement.click/en/welcome");
        driver.findElement(By.xpath("//a[text()='About Me']")).click();
    }

    @AfterEach
    public void tearDown(){
        if(driver != null){
            driver.quit();
        }
    }


}

In this article we’ll check how to cover driver.get, driver.findElement and WebElement.click with logging. The actual capabilities of the solution we’re going to review are far more reach but for the sake of shortness we’ll check only three mentioned actions. We will add text logging to WebDriver methods and "screenshot" logging to WebElement methods so that we’ll be taking screenshot before and after our tests preform each click operation against WebElement.

EventFiringWebDriver as Selenium standard for action logging in Java

Selenium bindings for Java provide EventFiringWebDriver class that is to wrap a regular WebDriver acting as decorator. For some methods it wraps the calls with some preceding code and some subsequent code that is implemented as listeners. But for some - it doesn’t. It also decorates all WebElement elements which are returned by findElement and findElements methods of that decorated driver. Elements and drivers are to utilize the same listener objects which have to implement WebDriverEventListener interface.

Yet it might look too confusing. We’re look at the example later on in the article. It will sort everything out.

So yet we can just say that in order to accomplish the goal we have stated for this article we need to:

  1. Somehow implement a listener that would contain methods which are to be invoked before or/and after some our actions with WebDriver. That code would log text entries and make screenshots of current screen.

  2. Wrap our existing driver with EventFiringWebDriver and register our listener(s)

  3. Somehow substitute WebDriver that is used by our tests with our wrapper.

As you might have noticed we can register several listeners which in turn might have handlers for the same actions. The general rule is that all listeners are invoke handlers for the action in the order they have been registered.

Almost all the actions you can do with Selenium are supported. You can observe the methods that WebDriverEventListener interface defines. There are 13 pairs of methods "before"/"after" for both WebDriver and WebElement related actions and one method that defines the handler for exceptions.

Implement WebDriverEventListener with our custom handlers

Alright. Let’s recall what exactly we need to log so that we can implement appropriate handlers:

  • Log when we navigate to somewhere using either driver.navigate() methods or driver.get(). In this case we will create a text log entry and a screenshot after the navigation has been completed.

  • Log when we attempt to find an element using driver.findElement. In this case we will create a text log entry and a screenshot before we start looking element up.

  • Log when we attempt to click an element. In this case we will create a text log entry and a screenshot before and after the click.

Lets look into WebDriverEventListener interface again. Here are the methods we’ll need to implement:

void afterNavigateTo(String url, WebDriver driver);
void beforeFindBy(By by, WebElement element, WebDriver driver);
void beforeClickOn(WebElement element, WebDriver driver);
void afterClickOn(WebElement element, WebDriver driver);

However we cannot just implement some methods of the interface and not implement others. Java does not work in that way. Fortunately Selenium developers prepared some default implementation of WebDriverEventListener that is provided with AbstractWebDriverEventListener class. The latter is the class that just provides "empty" implementation for all the interface methods. Also, since the class is abstract we cannot use it directly (Java won’t allow us to create an object of that class).

The approach that Selenium authors was implying is that we create our custom class that extends their basic implementation where we will override the only methods we need. This will really save time and space for us. Finally lets add the following class to our project:

package click.webelement.logging;

import org.openqa.selenium.*;
import org.openqa.selenium.support.events.AbstractWebDriverEventListener;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.UUID;

public class CustomLoggingListener extends AbstractWebDriverEventListener {

    private static final String SCREENSHOT_LOCATION = "/home/alexey/Desktop/Dev/testing/screenshots";

    @Override
    public void afterNavigateTo(String url, WebDriver driver) {
        String messageId = UUID.randomUUID().toString();
        System.out.println(messageId + " : Navigating to [" + url + "] with driver [" + driver + "]");
        takeScreenShot(messageId, driver);
    }

    @Override
    public void beforeFindBy(By by, WebElement element, WebDriver driver) {
        String messageId = UUID.randomUUID().toString();
        System.out.println(messageId + " : Try to locate element using [" + by + "] and driver [" + driver + "] and element [" + element + "]");
        takeScreenShot(messageId, driver);
    }

    @Override
    public void beforeClickOn(WebElement element, WebDriver driver) {
        String messageId = UUID.randomUUID().toString();
        System.out.println(messageId + " : Clicking element [" + element + "] with driver [" + driver + "]");
        takeScreenShot(messageId, driver);
    }

    @Override
    public void afterClickOn(WebElement element, WebDriver driver) {
        String messageId = UUID.randomUUID().toString();
        System.out.println(messageId + " : Clicked element [" + element + "] with driver [" + driver + "]");
        takeScreenShot(messageId, driver);
    }

    private void takeScreenShot(String name, WebDriver driver){
        File src = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE);
        try {
            FileChannel srcChannel = new FileInputStream(src).getChannel();
            File dst = new File(SCREENSHOT_LOCATION, name + ".png");
            FileChannel dstChannel = new FileOutputStream(dst).getChannel();
            dstChannel.transferFrom(srcChannel, 0, srcChannel.size());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

Here let me add couple of words explaining what is going on in the code. First of all, of course, you need to change SCREENSHOT_LOCATION to the path that is relevant to your environment.

Another thing is takeScreenShot method. It takes a file name (notice that no extension is required), adds .png to that name and creates a screenshot (if your WebDriver supports such feature). Except of mentioned specific there is nothing else requiring our attention since the method is quite regular for such kind of task.

Moving on to the handling code that is annotated with @Override, here are few things to consider. First of all I’m logging text messages with System.out.println() which does not look really nice, however this makes the entire example simpler. Next thing is that when you create a file, you are limited with the acceptable characters to be used in your file name. This is why I decided not to include any information from "drivers", "elements" and "bys" which are supplied to our listener by EventFiringWebDriver. Instead I generate UUID random string that marks each log message. Using that UUID value from the text log you will easily associate the particular event from your test with the taken screenshot that will be named correspondingly.

Integrate our listeners to legacy test

There is one small thing left to do. Now we need to seamlessly substitute the driver we used before with our EventFiringWebDriver wrapper. This is really easy step. Since EventFiringWebDriver implements WebDriver interface, we can just re-assign the field with new value. So that we now have to change slightly our setUp() method:

Instead of this:

@BeforeEach
public void setUp(){
    driver = new FirefoxDriver();
}

You will have this:

@BeforeEach
public void setUp(){
    driver = new FirefoxDriver();
    EventFiringWebDriver eventFiringWebDriver = new EventFiringWebDriver(driver);
    eventFiringWebDriver.register(new CustomLoggingListener());
    driver = eventFiringWebDriver;
}

So what we have just done? With only one additional class and simple rework of how we create a driver, we introduced logging of all the actions we require with text and screenshots. Moreover we didn’t change the test code itself. So there is minimum of refactoring with maximum of value.

If you still have the questions please send them to me using this form. I will amend the article according to your feedback.