Pages

Saturday, 6 July 2024

Spring Boot TestContainers with Selenium and Chrome



In this Tutorial

We will do the following:

  1. Use TestContainers to test a public website,
  2. Use TestContainers to test a localhost website with a port.

GitHub Repository

If you're just here for the GitHub Repo, here it is: github.com/lukegjpotter/SeleniumDemos.

The key files are the

  • build.gradle [link],
  • TestContainersTest.java [link], and
  • LocalHostServerPortControllerTest.java [link].

Explainer

If you're here for an explainer...

The current build, at publication time, of TestContainers is 1.19.8.

I've been playing around with Docker and Spring Boot, and more importantly Selenium Tests. Selenium has support for RemoteWebBrowser and the TestContainers project has support for spinning up Chrome in a Docker Container.

The benefit here is that it removes the need for installing and maintaining Edge, Firefox and/or "Chrome for Testing" (since July 2023, Chrome 115) installs on the Test Machine. When you install a new version of "Chrome for Testing", on the first launch, you are presented MacOS security and then a Google login page and some more stuff that you don't want.

Running the Browser in a Docker Container removes this slight pain point. And almost makes it plug-and-play.

TestContainers will download a Browser Docker Image that is compatible with your Selenium version.


You will need:

  • a fast internet connection, as TestContainers will download about 2GB of Docker Images on first run.
  • Docker on your machine, e.g. Docker Desktop on MacOS


If you want to get ahead of the 2GB download of Docker Images, you will need:

The version tags are as of the time of writing, 6th July 2024. But, as stated above, TestContainers will pull the correct versions for you.

lukegjpotter@Lukes-MacBook-Pro ~ % docker pull alpine:3.17 \
&& docker pull selenium/standalone-chrome:4.19.1 \
&& docker pull testcontainers/ryuk:0.7.0 \
&& docker pull testcontainers/vnc-recorder:1.3.0


So you end up with the following

lukegjpotter@Lukes-MacBook-Pro ~ % docker images           
REPOSITORY                    TAG     IMAGE ID       CREATED         SIZE
alpine                        3.17    06929782def5   2 weeks ago     7.08MB
selenium/standalone-chrome    4.19.1  cd287a41194d   3 months ago    1.19GB
testcontainers/ryuk           0.7.0   d2ed07d3edac   3 months ago    15.2MB
testcontainers/vnc-recorder   1.3.0   35a6350968be   19 months ago   686MB


So let's get started on the Spring Initializer, start.spring.io. First off, you don't need any of the Dependencies. The actual "testcontainers" Spring Boot dependency mainly has Annotations for Service Connections for Databases, Message Queues and Kafka. Spring Boot Documentation: Testcontainers -> Service Connections. Thusly we don't need it for Selenium.

As we only need the project structure and Metadata, and no dependencies, from the Spring Initializer, we Generate the project and open it in the IDE. We'll add the required Dependencies to the build.gradle:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.testcontainers:testcontainers'
    testImplementation 'org.testcontainers:junit-jupiter'
    testImplementation 'org.testcontainers:selenium'
    testImplementation 'org.seleniumhq.selenium:selenium-java:4.22.0'

    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}


Next we will create the test class: TestContainersTest.java

Our packages are:

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.springframework.boot.test.context.SpringBootTest;
import org.testcontainers.containers.BrowserWebDriverContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.io.File;
import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;


The Class Annotations are:

@Testcontainers
@SpringBootTest
public class TestContainersTest {


We declare our Container, the output directory of the failing recordings, and RemoteWebDriver as follows:

private static final File recordingsOutput = new File("./build/test-recordings/");

@Container
private final BrowserWebDriverContainer<?> BrowserWebDriverContainer = 
         new BrowserWebDriverContainer<>()
            .withRecordingMode(VncRecordingMode.SKIP, 
                               recordingsOutput);

private RemoteWebDriver driver;


We're skipping the Recordings, they serve little purpose here. For more complicated tested, you can use VncRecordingMode.RECORD_FAILING.


The SetUp and TearDown methods look like this: if you just want the BeforeAll and AfterAll methods, just declare the Container and RemoteWebDriver as static. We need to actually create the output directory for the VNC Recordings, as it is not created otherwise.

@BeforeAll
static void beforeAll() {
    if (!recordingsOutput.exists()) recordingsOutput.mkdirs();
}

@BeforeEach
void setUp() {
    driver = new RemoteWebDriver(
          BrowserWebDriverContainer.getSeleniumAddress(), 
          new ChromeOptions());
    driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30));
}

@AfterEach
void cleanUp() {
    driver.quit();
}


Finally, our Test is pretty basic with AssertJ (comes as part of the SprintBootTest annotation):

@Test
void phpTravels_PageTitle() {
    driver.get("https://phptravels.com/demo/");
    String pageTitle = driver.getTitle();

    assertThat(pageTitle)
         .isEqualTo("Book Your Free Demo Now - Phptravels");
}


Run the Test, and unless you've downloaded the Images in advance, the first run will take a very long time for the Docker Images to be downloaded. You'll also need Docker running on your machine whilst running these tests.


What's Next?

You can also add the LocalServerPort if you need to test on localhost.

But I'll save that for another blog post, as I want that Google Ads Revenue, although given the target audience, you're all using the AdBlockers. :)

Just Joking

We add the Spring Boot Web Dependency to the build.gradle, just add "-web" to the Spring Boot Starter. This will give us Annotations suchs as @RestController and @GetMapping.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'


Refresh Gradle, or Maven, if you're old school.

Next add a very rough (no code reviews on this section please) GET method that returns some raw HTML (this Class goes in the src/main/java/... directory):

@RestController
@RequestMapping("/html")
public class LocalHostServerPortController {

    @GetMapping("/simplehtml")
    public ResponseEntity<String> getSimpleHtml() {
        return ResponseEntity.ok("<html><head>" 
              + "<title>Local Test Works!!</title>" 
              + "</head><body>Testing on Local</body></html>");
    }
}


And we create a new, or update our existing, Selenium Test Class:

@Testcontainers
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class LocalHostServerPortControllerTest {

    private static final File recordingsOutput = new File("./build/test-recordings/");

    @Container
    private final BrowserWebDriverContainer BrowserWebDriverContainer<?> 
             = new BrowserWebDriverContainer<>()
             .withRecordingMode(VncRecordingMode.SKIP, 
                                recordingsOutput);

    private RemoteWebDriver driver;

    @LocalServerPort
    private int localServerPort;

    @BeforeAll
    static void beforeAll() {
        if (!recordingsOutput.exists()) recordingsOutput.mkdirs();
    }

    @BeforeEach
    void setUp() {
        driver = new RemoteWebDriver(
             BrowserWebDriverContainer.getSeleniumAddress(), new ChromeOptions());
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30));
    }

    @AfterEach
    void cleanUp() {
        driver.quit();
    }

    @Test
    void localhostPortTest() {
        driver.get(
            "http://host.docker.internal:" 
            + localServerPort 
            + "/html/simplehtml");
        String pageTitle = driver.getTitle();

        assertThat(pageTitle).isEqualTo("Local Test Works!!");
    }
}


In this updated class, we add the WebEnvironment.RANDOM_PORT, then assign it to a variable via the @LocalServerPort. Finally the Driver URL is host.docker.internal and the LocalServerPort, then append the Endpoint.


Happy Testing,
Luke

No comments:

Post a Comment

Note: only a member of this blog may post a comment.