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