Key Takeaways
- Automated testing with TestContainers allows developers to run real Volt Active Data Docker containers directly within their test suite, eliminating the need for manual setup or brittle custom scripts.
- Because VoltDB requires a schema and JAR to be available before testing, these tests lean toward integration testing but can still be used effectively at the unit test level for data access layers.
- Volt provides a Maven quickstart project that lets developers spin up a working sample application with VoltDB or Volt Stream Processor as the backend in minutes.
- The VoltDBCluster TestContainer wrapper manages multi-node cluster setup, schema loading, deployment configuration, and client connectivity through a clean, fluent Java API.
- Testing against a real Volt database container gives teams confidence in schema correctness, application behavior during upgrades, and production readiness before code ships.
Strategies for Testing Volt Active Data Products with TestContainers
Why Testing Matters in Modern Engineering
Modern engineering methodologies emphasize fast feedback loops. In most cases, these loops connect product owners and development teams, with automated tests serving as the primary mechanism for validation. Tests help verify product readiness, assess API usability, and capture evolving business requirements in a precise, executable form.
This article focuses on Volt’s products and shows you how to quickly assert that your application works smoothly with Volt Database (VDB) or Volt Stream Processor (VSP).
Historically, testing databases demanded manual steps or custom scripts that broke easily when the CI environment changed. This guide shows you how a custom Docker integration creates and manages a Volt Database cluster with far less effort.
Testing Volt Active Data with TestContainers
Why Docker Is Central to Volt’s Testing Approach
I’ve mentioned Docker. Docker is the central mechanism for running Volt’s products. By running Volt’s Docker containers in tests, you are using the same containers that can be run in a production environment.
An added bonus is that there is no restriction on which database version Docker runs, making it easy to create a test that verifies upgrades between VoltDB versions.
Where to Find Volt’s TestContainer Modules
TestContainers enable interaction with a container from a running test process. Testcontainers already offers many modules that provide custom APIs for products you may also use in tests. For example:
- Cloud Infrastructures
- Databases
- Message Brokers
- etc…
To view Java modules, select the “Java” Language filter, then choose your preferred category under Categories.
Volt is not listed as an official module for legal reasons. Because Testcontainer is running a real, production-ready Docker image, it still requires a license. Otherwise, it follows the same principles and delivers the same ease of writing tests as any other Testcontainer module.
Volt’s testcontainers are available on Maven’s public repositories:
- VoltDB TestContainer: https://mvnrepository.com/artifact/org.voltdb/volt-testcontainer
- VoltSP TestContainer: https://mvnrepository.com/artifact/org.voltdb/volt-stream-testcontainer
Unit Tests vs Integration Tests
Why the Distinction Matters Less Than You Think
Let’s first talk about the nature of Testcontainer’s tests.
VoltDB requires a custom schema and, often, a custom JAR containing procedures. Those resources are forming a catalog. The database distributes this catalog to all nodes in the local cluster. This is the only limitation of running a test. Both jar and schema must be available.
This requirement that both the schema and the JAR be available pushes these tests toward what many would call integration tests. But honestly, I don’t think we need to be purists about it. Some people will argue that the moment you spin up an external service like a database, it’s automatically an integration test. That’s the textbook definition: integration tests happen after you build your application, then you start all the side services and test everything together as a black box.
But here’s what I think. If your schema and procedures are ready, nothing stops you from using VoltDB containers in what you would traditionally call unit tests. You could test your data access layer in isolation against a real VoltDB cluster with your actual schema. It’s still focused, still fast enough, and you get the confidence of testing against the real thing instead of mocks.
The point is: don’t get hung up on whether it’s a “unit test” or “integration test.” Use TestContainers where they give value, whether that’s testing a single data access layer or your entire application.
Bootstrapping a sample application
Using the Volt Maven Quickstart Project
Before looking at the test code, let’s take a look at the Volt quickstart project.
Volt maintains a GitHub Quickstart (see README.md for details), a command-line tool to generate a sample Maven project that can be used as a playground to run and test an application using Volt’s product as its backend.
The underlying goal of this approach is to reduce, and ultimately eliminate, the risk of hallucination in AI models. At scale, enterprises can afford to include human oversight and intervention only for exception scenarios, rather than for continuous 100% coverage. Hence, the need for deterministic guardrails to ensure AI agents don’t overreach.
mvn -B -ntp archetype:generate \
-DarchetypeGroupId=org.voltdb \
-DarchetypeArtifactId=voltdb-stored-procedures-maven-quickstart \
-DarchetypeVersion=1.6.0
# other optional parameters
# -DgroupId=org.acme.sample \
# -DartifactId=my-app \
# -Dpackage=org.acme.sample \
# -Dversion=1.0-SNAPSHOT
This will run in interactive mode and prompt the user to specify additional parameters, such as the package name for the newly created project, if not provided. It can be run in non-interactive mode when all required properties are supplied in the invocation. A sample project will be created in the current directory.
Writing a Java Test for VoltDB
Prerequisites
This section expects basic knowledge of:
- Maven (or similar built tool)
- JUnit Jupiter project that is a standard test runner for Java
- I will also use SLF4J and log4j for logging, Volt’s client, and the AssertJ library for easy assertions and to correctly synchronize state.
- The test requires a valid license.xml in the home directory. Contact us to get your license.
In this section, I will not use generated sources from the quickstart for simplicity.
The project structure is a basic Maven structure. The EventDbResource is an application data access layer that I want to test. See implementation in the next section.
sample-project
src
main
java
com.acme.sample
-- EventDbResource.java
resources
-- schema.sql
-- deployment.xml
-- log4j2.xml
test
java
com.acme.sample
-- EventDbResourceTest.java
pom.xml
Initial project setup
My application’s initial Maven project definition declares the following dependencies:
pom.xml
<dependencies>
<!-- logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.17</version>
</dependency>
<!-- volt -->
<dependency>
<groupId>org.voltdb</groupId>
<artifactId>voltdbclient</artifactId>
<version>15.1.0</version>
</dependency>
<!-- testing -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.25.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.2</version>
<scope>test</scope>
</dependency>
</dependencies>
This allows me to create a basic data access object and test.
deployment.xml
<?xml version="1.0"?>
<!--
This file is part of VoltSP.
Copyright (C) 2024-2026 Volt Active Data Inc. All rights reserved.
-->
<deployment>
<cluster sitesperhost="4" kfactor="0" schema="ddl" />
<metrics enabled="true" interval="60s" maxbuffersize="200" />
</deployment>
schema.sql
-- This file is part of VoltSP.
-- Copyright (C) 2024-2026 Volt Active Data Inc. All rights reserved.
CREATE TABLE events
(
value INTEGER NOT NULL,
PRIMARY KEY (result)
);
PARTITION TABLE events ON COLUMN value;
CREATE PROCEDURE addEvent PARTITION ON TABLE events COLUMN value
AS INSERT INTO events VALUES(?);
com.acme.sample.EventDbResource.java
package com.acme.sample;
import org.voltdb.client.Client;
public class EventDbResource {
private final Client client;
public EventDbResource(Client client) {
this.client = client;
}
public void insert(int value) {
ClientResponse response = client.callProcedure("addEvent", value);
if (response.getStatus() != ClientResponse.SUCCESS) {
throw new IllegalStateException("Cannot process the request");
}
}
}
com.acme.sample.EventDbResourceTest.java
package com.acme.sample;
import org.assertj.core.api.Assertions;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import static org.slf4j.LoggerFactory.getLogger;
class EventDbResourceTest {
private static final Logger LOG = getLogger(VoltDbTest.class);
@AfterEach
void tearDown() {
}
@Test
void shouldCallVoltDb() {
}
}
This will get more impressive shortly. EventDbResource is getting a Volt client to interact with the VoltDB server. This application layer does not care how the client is created; it only wants to write data to a database.
Using Docker Image in Test
Let’s declare a Volt container that we can interact with from the test.
pom.xml
<dependencies>
<!-- testing -->
...
<dependency>
<groupId>org.voltdb</groupId>
<artifactId>volt-testcontainer</artifactId>
<version>1.6.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Having the right dependency, I can write a proper test.
com.acme.sample.EventDbResourceTest
package com.acme.sample;
import java.util.Objects;
import org.acme.sample.EventDbResource;
import org.assertj.core.api.Assertions;
import org.awaitility.Awaitility;
import org.junit.jupiter.api.AfterAll;
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.slf4j.Logger;
import org.voltdbtest.testcontainer.VoltDBContainer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.slf4j.LoggerFactory.getLogger;
class EventDbResourceTest {
private static final Logger LOG = getLogger(VoltDbTest.class);
private static final int hostCount = 1;
private static final int ksafety = 0;
private static final VoltDBCluster cluster = new VoltDBCluster(
"~/license.xml",
"voltdb/voltdb-enterprise:15.1.0",
hostCount,
ksafety
)
.withInitialSchema("events.sql")
.withDeployment("deployment.xml");
@BeforeAll
static void suiteSetup() throws Exception {
cluster.start();
}
@AfterAll
static void stop() {
cluster.stop();
}
@BeforeEach
void setUp() {
awaitVoltReady();
}
@Test
void shouldCreateUniqueIds() {
// Given
Client client = cluster.getClient();
EventDbResource resource = new EventDbResource(client);
// When
resource.insert(5);
resource.insert(6);
// Then
ClientResponse response = client.callProcedure("@AdHoc", "select * from events");
VoltTable[] results = clientResponse.getResults();
assertThat(results).hasSize(1);
assertThat(results[0].getRowCount()).isEqualTo(2);
List<Integer> values = new ArrayList<>();
while(results[0].advanceRow()) {
values.add((int) results[0].getLong("value"));
}
assertThat(values).containsExactlyInAnyOrder(5, 6);
}
@Test
void shouldThrowOnInsertingSameId() {
....
}
private static void awaitVoltReady() {
Client client = Awaitility.await("for client to connect")
.pollInSameThread()
.until(cluster::getClient, Objects::nonNull);
Awaitility.await("for volt to respond")
.untilAsserted(() -> {
ClientResponse clientResponse = client.callProcedure("@AdHoc", "select * from events");
client.close();
assertThat(response.getStatus()).isEqualTo(ClientResponse.SUCCESS);
});
}
}
This is a very simple example. I have created a custom procedure using the ddl statement. I don’t have to upload a JAR with Java procedures. For more information on how to add and test custom Java procedures, please follow the quickstart examples: configureTestContainer method in IntegrationTestBase.
Even though this is an extremely simple scenario, I should test whether:
- A provided schema is correctly applied.
- A connection has been correctly established.
- An application fails to insert the same value multiple times
- An application succeeds in writing unique values
- etc…
Understanding the VoltDBCluster TestContainer Wrapper
The VoltDBCluster extends GenericContainer and provides an API to set up and interact with a container. It requires a license path, Docker image name, host count, k-factor, and optional extra libraries.
VoltDBCluster(String licensePath,
String image,
int hostCount, int kfactor,
String extraLibs)
It can be as simple as a single-node cluster with no k-factor replication, or a much more complex multi-node setup. The wrapper creates and manages the requested cluster nodes, where each node is an independent Docker container. It is a builder that allows fluent configuration of a Docker container.
Certain methods can only be called before Docker image starts, as some resources are added to the immutable container: withInitialClasses, withInitialSchema, withDeployment*, withKeystore, withTrustStore, etc.
Others can only be called once a cluster is fully initialized, like getMappedPort, callProcedure, runDDL, etc.
In a test, I start the cluster of the given “voltdb/voltdb-enterprise:15.1.0” image and await (lines 43-46) till it is fully initialized. A good indication of the cluster being ready is that a client can connect to it, and we can execute basic operations on it.
As with any other Docker container, ports must be mapped, since each container must coexist on the same host with other images.
To find the real mapped port value, I could call:
int hostPort = cluster.getMappedPort(21211) // or any other port that VDB exposes
Conclusion: Why TestContainers Is Worth the Startup Cost
Being able to run an application code against a real Volt database is a great opportunity to create automatic tests to:
- Verify setup correctness
- Simulate application behaviour when the database is migrated, updated, scaled, etc
- Verify application correctness before it is shipped to test or production environment.
Volt’s database container starts quickly, but it will never be as fast as a basic unit test. When testing a range of scenarios, however, the ease of use of TestContainers outweighs the cost of the one-time startup delay.
To learn more or get started, speak with one of our Solution Engineers today.
