TestContainers made simple with JBang

Combining JBang with Testcontainers and JUnit 5 enables writing lightweight, zero-boilerplate Java test scripts that run real Docker containers directly from the command line. This approach eliminates the need for full Maven or Gradle project setups when building quick proof-of-concept tests for microservices or container images. In this tutorial, you will learn how to create and execute containerized integration tests using JBang.

Things we need to know

Modern software development relies on a diverse set of dependencies, services, and databases. Testing applications in a consistent, isolated, and reproducible environment can be challenging. This is where Testcontainers comes into play. Testcontainers is an open-source Java library that simplifies the integration testing of applications by providing lightweight, disposable containers for various dependencies. The following article provides a comprehensive introduction to TestContainers: Getting Started with Testcontainers for Java .

JBang is a tool that allows you to run Java programs directly from source files. It also simplifies the configuration and dependency management. To learn more about JBang, you can check this article: JBang: Create Java scripts like a pro

By combining TestContainers and JBang we can easily code scripts for testing container images without the need to set up a project for that.

Coding a simple Container Test

We will code a simple Testcontainers example that runs a Test on the Container image of Nginx Web Server: Let’s begin from the Container definition:

@Container
static GenericContainer container = new GenericContainer<>(DockerImageName.parse("nginx"))
            .withExposedPorts(80);
  • An Nginx container is defined with the GenericContainer class, specifying the Docker image “nginx.”
  • The container is configured to expose port 80, allowing external access to the Nginx server running inside the container.

Let’s see the full code:

///usr/bin/env jbang "$0" "$@" ; exit $? 

//JAVA 17

//DEPS org.testcontainers:testcontainers-bom:1.17.6@pom
//DEPS org.testcontainers:testcontainers
//DEPS org.testcontainers:junit-jupiter
//DEPS org.junit:junit-bom:5.9.1@pom
//DEPS org.junit.jupiter:junit-jupiter-api
//DEPS org.junit.jupiter:junit-jupiter-engine
//DEPS org.junit.platform:junit-platform-launcher
//DEPS org.junit.platform:junit-platform-reporting
//DEPS org.assertj:assertj-core:3.23.1

import com.github.dockerjava.api.model.ExposedPort;
import org.junit.jupiter.api.Test;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.LoggingListener;
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

import static java.lang.System.out;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;

@Testcontainers
class testnginx {

	@Container
	static GenericContainer container = new GenericContainer<>(DockerImageName.parse("nginx"))
			.withExposedPorts(80);

	@Test
	void testNginxSaysWelcome() throws IOException, InterruptedException {
		var client = HttpClient.newBuilder()
				.version(HttpClient.Version.HTTP_2)
				.followRedirects(HttpClient.Redirect.NORMAL)
				.connectTimeout(Duration.ofSeconds(5))
				.build();

		var request = HttpRequest.newBuilder()
				.uri(URI.create(String.format("http://%s:%d/", container.getHost(), container.getFirstMappedPort())))
				.timeout(Duration.ofSeconds(5))
				.GET()
				.build();

		var response = client.send(request, HttpResponse.BodyHandlers.ofString());

		assertThat(response.statusCode()).isEqualTo(200);
		assertThat(response.body()).contains("Welcome to nginx!");
	}

	public static void main(final String... args) {
		var request = LauncherDiscoveryRequestBuilder.request()
				.selectors(selectClass(testnginx.class))
				.build();

		var launcher = LauncherFactory.create();
		var logListener = LoggingListener.forBiConsumer((t, m) -> {
			System.out.println(m.get());
			if (t != null) {
				t.printStackTrace();
			}
		});
		var execListener = new SummaryGeneratingListener();
		launcher.registerTestExecutionListeners(execListener, logListener);
		launcher.execute(request);
		execListener.getSummary().printTo(new java.io.PrintWriter(out));

		if (execListener.getSummary().getTotalFailureCount() > 0) {
			System.exit(1);
		}

		System.exit(0);
	}
}

In essence, this Test Class sends an HTTP Request on the Nginx Server. Then it verifies that the Welcome message matches with the default one.

Finally, it is worth mentioning that the main method uses the JUnit Platform Launcher API to gain more control over the test execution process. It allows the script to capture and log detailed information about the test execution, including individual test outcomes and any exceptions that may occur during the test run.

You can run it with:

jbang testnginx

Your Container will start right away:

testcontainer example

Then, check the output of the Test Report:

testcontainers with jbang tutorial

Conclusion

You have successfully run a Testcontainers-based test for an Nginx container using JBang. This approach allows you to easily experiment with Testcontainers tests without the need for a complex build setup. Feel free to customize the script or explore additional Testcontainers features based on your testing needs.

Running Custom Container Scripts with Port Binding

When running a JBang Testcontainers script, dynamically mapped ports ensure no port conflicts occur on the host machine. Here is how to retrieve and interact with exposed container ports using Java's built-in HttpClient:

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 21
//DEPS org.testcontainers:testcontainers-bom:1.20.4@pom
//DEPS org.testcontainers:junit-jupiter
//DEPS org.junit:junit-bom:5.12.0@pom
//DEPS org.junit.jupiter:junit-jupiter-api
//DEPS org.junit.jupiter:junit-jupiter-engine
//DEPS org.junit.platform:junit-platform-launcher

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
import java.net.URI;
import java.net.http.*;

class ContainerScript {
    static GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:alpine"))
            .withExposedPorts(6379);
}

Recommended Articles

Enhance Your Java Tests with TestContainers: Set Up PostgreSQL and Generic Containers

Learn how to use TestContainers for robust database testing in Java applications. Includes setup guide and benefits of using Testcontainers.

Optimize Your Enterprise Java Applications with Testcontainers and Databases

Learn how to use Testcontainers for accurate database testing in your JPA/Hibernate applications. #TestContainers #Java #Databases #JPA #Hibernate

Run CLI Commands in WildFly Docker Images with Embed-Server Feature

Learn how to run CLI commands when building Docker images of WildFly application server using the embed-server feature.

Manage Docker Images from Eclipse JBoss Tools

Learn how to use Eclipse JBoss tools to manage Docker images and deploy applications on top of them