My task is to test a dockerized web application using Selenium. It is important that the tests are defined with Gherkin and of course run headless on Jenkins. Here is what I did to achieve this task.

Cucumber And Testcontainer Specifics

This is a snippet from the class responsible to pull and instantiate the Webapp container which contains the build of my webapp to test:

@Slf4j
public class WebappContainer {

    private static WebappContainer instance = new WebappContainer();

    private static final String NETWORK_ALIAS = "WEBAPP";

    private static final int EXPOSED_PORT = 7654;

    private static final DockerImageName dockerImageName = DockerImageName
            .parse("myecr.amazonaws.com/webapp/my-little-webapp:"
                    + getWebappImageVersion());

    public static final GenericContainer<?> container = new GenericContainer<>(dockerImageName)
            .withNetwork(NetworkUtils.getNetwork())
            .withNetworkAliases(NETWORK_ALIAS)
            .waitingFor(Wait.forHttp("/").forStatusCode(200).forPort(PORT)
                    .withStartupTimeout(Duration.ofSeconds(STARTUP_TIMEOUT)))
            .withExposedPorts(EXPOSED_PORT)
            .withLogConsumer(new Slf4jLogConsumer(log))
            // next line maybe specific to my setup: Mount Spring Boot test config into container
            .withClasspathResourceMapping("application-test.yml", "/etc/config/application.yml", 
                    BindMode.READ_ONLY);

    private WebappContainer() {
    }

    public static WebappContainer getInstance() {
        return instance;
    }

    public void start() {
        container.start();
    }

    public void stop() {
        container.stop();
    }

    public boolean isRunning() {
        return container.isRunning();
    }

    public int getHttpPort() {
        return container.getMappedPort(EXPOSED_PORT);
    }
}

For completeness sake here the static helper method to get the image

public static String getWebappImageVersion() {
        // this may be set by Jenkins to a specific image tag
        String imageVersion = System.getenv("WEBAPP_IMAGE_VERSION");
        return StringUtils.isNotBlank(imageVersion) ? imageVersion : "latest";
}

Chrome Container Definiton

The class which defines the headless chrome container:

public class ChromeWebDriverContainer {

    private static final ChromeWebDriverContainer instance = new ChromeWebDriverContainer();

    public static final BrowserWebDriverContainer<?> chrome = new BrowserWebDriverContainer<>()
            .withCapabilities(chromeOptions());

    private ChromeWebDriverContainer() {
    }

    // had to set these options or else the strangest errors appeared
    // while starting the container
    private static Capabilities chromeOptions() {
        ChromeOptions chromeOptions = new ChromeOptions();
        chromeOptions.addArguments("--headless", "--no-sandbox", "--disable-dev-shm-usage");
        return chromeOptions;
    }

    public static ChromeWebDriverContainer getInstance() {
        return instance;
    }

    public void start() {
        chrome.start();
    }

    public void stop() {
        chrome.stop();
    }

    public boolean isRunning() {
        return chrome.isRunning();
    }

    public int getHttpPort() {
        return chrome.getMappedPort(4444);
    }

    public RemoteWebDriver getRemoteWebDriver() {
        return chrome.getWebDriver();
    }
}

The Test

An example test implementation which uses the containers defined above:

@Cucumber
public class SimpleTest {
    
    private RemoteWebDriver driver;
    
    @Given("^Chrome is running$")
    public void chrome_is_running() {
        ChromeWebDriverContainer.getInstance().start();
        this.driver = ChromeWebDriverContainer.getInstance().getRemoteWebDriver();
    }

    @Given("^Webapp is running$")
    public void webapp_is_running() {
        WebappContainer.getInstance().start();
    }

    @When("^I visit the webapp start page$")
    public void visit_the_webapp_start_page() {
        // this line is quite important. The chrome container needs access to the webapp container,
        // which exposes a port on the docker host
        // see NetworkUtils snippet below
        driver.get("http://" + NetworkUtils.determineLocalIpAddress() + ":"
                + WebappContainer.getInstance().getHttpPort());
    }

    @When("^I klick 'Send'$")
    public void klick_send() throws InterruptedException {
        driver.findElement(By.cssSelector("some css button selector")).click();
    }

    @Then("a message with title {word} should appear")
    public void errormessage_should_appear(String title) {
        // it may take some time for the modal dialogue to appear, so wait for it
        WebDriverWait wait = new WebDriverWait(ChromeWebDriverContainer.getInstance().getWebDriver(), 30);
        wait.until(ExpectedConditions.textToBe(By.cssSelector(".modal-title"), title));
    }
}

How to determine the docker hosts ip address:

@SneakyThrows
public static String determineLocalIpAddress() {
    try (final DatagramSocket socket = new DatagramSocket()) {
        // it doesn't matter that this external ip may not be reachable...but at least 
        // the socket will be opened through the default gateway and now we
        // can be quite sure that this is the right network interface
        socket.connect(InetAddress.getByName("8.8.8.8"), 5000);
        return socket.getLocalAddress().getHostAddress();
    } catch (UnknownHostException | SocketException e) {
        try {
            // fallback
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e1) {
            log.error("unable to determine local ip address", e);
        }
    }
    return null;
}

Cucumber Feature Definition

And finally stitching it all together with this feature definition

Feature: My simple webapp feature

Scenario: I want to send some data
Given Webapp is running
Given Chrome is running

When I visit the webapp start page
And I klick 'Send'

Then a message with title Success should appear