Test a page for broken images and other resources with Selenium and BrowserMob-Proxy in Java

Sometimes in our automated tests in Selenium we need to test a page not just for some functionality (how proper the response for the user action is) but also for whether the resources of a page have been loaded properly. For example we might need to make sure the server returns proper response for the following resources:

  • images

  • video files

  • CSS files

  • JavaScript files

  • etc

The approach we are going to review here can also be useful when we run automated test of some AJAX page that tries to perform asynchronous requests but receives error from the server. In such case the result of a failure won’t necessarily be visible on UI so having the knowledge you will obtain after reading this article will allow you to combine the methods in order to increase the efficiency of your tests.

There are few points we are going to cover below:

Let’s now take a closer look at them.

Example description: make Selenium automated test fail if an image failed to load

In our example we’re prepare two html pages: one that will be having a valid reference to a picture and another one that will not. Two pages are to be used in order to demonstrate that the test that will be testing a valid page will pass successfully. The second test will obviously have to fail.

Below you can see the code of both the pages.

I suggest you to create a dedicated folder for keeping the files from this tutorial.

Page that has valid picture

<html>
  <head/>
  <body>
    <image src="https://webelement.click/assets/images/ru.png"/>
    <image src="https://webelement.click/assets/images/en.png"/>
  </body>
</html>

Page that has broken picture

<html>
  <head/>
  <body>
    <image src="https://webelement.click/assets/images/ru.png"/>
    <image src="https://webelement.click/assets/images/eng.png"/>
  </body>
</html>

Save the first page as good.html and the second one as bad.html (later in the article I assume that we put our pages to /tmp folder - consider this when you adopt the example to your environment).

General description of the test method

Unfortunately Selenium by itself cannot (in most of the cases) detect if anything goes wrong on http(s) request level. Thus you will only know that some resource has not been properly loaded if you intentionally look up the element containing a resource and check certain properties of it. However the latter might still not work in all the cases.

The more robust and convenient method would be to intercept the responses which are sent by the browser at the moment of processing your page resources. This can be easily done with using a controllable proxy like BrowserMob-Proxy. The easiest way of making your code access BrowserMob-Proxy functionality is adding a dependency to your pom.xml file (you have to use Maven of course). Below is the dependency that I use in my example:

<dependency>
  <groupId>net.lightbody.bmp</groupId>
  <artifactId>browsermob-core</artifactId>
  <version>2.1.5</version>
  <scope>test</scope>
</dependency>

Having your WebDriver configured to use such proxy you will be able to monitor the responses that the server sends for the requests coming from the browser under your automated test control.

Another example of your Selenium automated tests (written in Java) integration with BrowserMob-Proxy lib you can find in my article were I show how to modify HTTP headers of the requests sent by browsers under Selenium control.

Implementing the test

Below you can find the complete example of the test that we’ll discuss in details right after the code snippet finishes. The boilerplate part assumes that we are using JUnit5 testing framework however the same can be implemented with TestNG or with pure main() approach.

package click.webelement.monitorrequests;

import net.lightbody.bmp.BrowserMobProxy;
import net.lightbody.bmp.BrowserMobProxyServer;
import net.lightbody.bmp.client.ClientUtil;
import org.junit.jupiter.api.*;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.firefox.FirefoxOptions;

import java.util.ArrayList;
import java.util.List;

public class MonitorRequestsTest {

    WebDriver driver;
    static BrowserMobProxy proxy;
    static List<String> failedURIs = new ArrayList<>();

    @BeforeAll
    public static void globalSetup(){
        System.setProperty("webdriver.gecko.driver"
                , "/home/alexey/Desktop/Dev/webdrivers/geckodriver");
        proxy = new BrowserMobProxyServer();
        proxy.start(0);
        proxy.addResponseFilter((httpResponse, httpMessageContents, httpMessageInfo) -> {
            if(httpMessageInfo.getOriginalRequest().headers().get("Accept").contains("image/")){
                int responseCode = httpResponse.getStatus().code();
                if(responseCode >= 400 && responseCode < 600){
                    failedURIs.add(httpMessageInfo.getOriginalRequest().getUri());
                }
            }
        });
    }

    @BeforeEach
    public void setUp(){
        FirefoxOptions ffOptions = new FirefoxOptions();
        ffOptions.setProxy(ClientUtil.createSeleniumProxy(proxy));
        driver = new FirefoxDriver(ffOptions);
        failedURIs.clear();
    }

    @Test
    public void testGoodPage(){
        driver.get("file:///tmp/good.html");
    }

    @Test
    public void testBadPage(){
        driver.get("file:///tmp/bad.html");
    }

    @AfterEach
    public void tearDown(){
        if(driver != null){
            driver.quit();
        }
        if(!failedURIs.isEmpty()){
            Assertions.fail("There were resource loading issues " +
                    "during the test " +
                    "for the following resources: " + failedURIs.toString());
        }
    }

    @AfterAll
    public static void globalTearDown(){
        if(proxy != null){
            proxy.stop();
        }
    }

}

Let’s now break this code down into pieces to get the method idea clearly. As you could already notice, except of the test methods which are really straightforward, we have four blocks here. They are @BeforeAll that is running once before all the tests, @BeforeEach that is running before each test, @AfterEach that is running after each test and @AfterAll that is executed once all the tests have completed.

Configure global set-up in @BeforeAll

This method brings the core idea to our tests. Here we start up the proxy that does not make sense to recreate for each new test (that is why we set it up in @BeforeAll section) and add a listener that would be listening the responses. Let’s look at this part:

proxy.addResponseFilter((httpResponse, httpMessageContents, httpMessageInfo) -> {
    if(httpMessageInfo.getOriginalRequest().headers().get("Accept").contains("image/")){
        int responseCode = httpResponse.getStatus().code();
        if(responseCode >= 400 && responseCode < 600){
            failedURIs.add(httpMessageInfo.getOriginalRequest().getUri());
        }
    }
});

Using addResponseFilter method you can process all the responses in order to (for example) modify them in any aspect. In our particular case we are going to use it to look for a specific response codes return in reply to requests from our automated tests. But first of all we need to limit (actually I need - you are free to implement your own logic) the checks with image resources. There are few ways of how we can decide if the request asked a server for an image. I prefer to parse Accept header of the request. Thus the browser informs a server that it expects to see the image in response.

After we have made sure we are processing a response returning an image we obtain the response code and check if it falls in the range from 400 (included) to 600 (excluded). This is how we detect if the request was not successful (codes like 4** mean that the error seems to originate on client side, codes like 5** point to some server-side issue).

The last thing we do in this block is adding the requested URI to the List field that is assumed to contain all the problematic requests. We’ll see how to deal with this list later.

Configure per-test set-up in @BeforeEach

Typical thing that is done in "before each" block is recreating a WebDriver. I adhere that practice in my example. However there is one more thing that we should stop at to look closer. I’m talking about this line:

failedURIs.clear();

Before each test we clean up the list that is used for holding problematic URIs so that the errors captured on previous tests would not impact the next tests.

Configure post-test polishing in @AfterEach

In this design the tests themselves are written in a regular Selenium manner like if we would not have the requirement to test particular HTTP requests. They might fail due to typical Selenium reasons like unavailability of the element or other UI issues.

After the test has finished JUnit5 executes @AfterEach block that checks if there were problematic resources detected during the test run:

if(!failedURIs.isEmpty()){
    Assertions.fail("There were resource loading issues " +
            "during the test " +
            "for the following resources: " + failedURIs.toString());
}

If the list is not empty by the end of the test run then the test is set forcibly failed. You should also notice that this section:

if(driver != null){
    driver.quit();
}

goes first. It is important because otherwise the failed test would cause code interruption and WebDriver won’t be ever disposed.

Configure global tear down with @AfterAll and run the tests

The last thing we need to do is to release our proxy so that it is stopped and it is not holding the port any more. After these last lines we can execute our tests. Below is the output that the code should produce:

org.opentest4j.AssertionFailedError: There were resource loading issues during the test for the following resources: [/assets/images/eng.png]

	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:39)
	at org.junit.jupiter.api.Assertions.fail(Assertions.java:109)
	at click.webelement.monitorrequests.MonitorRequestsTest.tearDown(MonitorRequestsTest.java:60)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

Using this approach you’ll be able to add any sort of HTTP-response validation to your Selenium automated tests. If you still have the questions please send them to me using this form. I will amend the article according to your feedback.