JVM | Platform | Status |
---|---|---|
OpenJDK (Temurin) Current | Linux | |
OpenJDK (Temurin) LTS | Linux | |
OpenJDK (Temurin) Current | Windows | |
OpenJDK (Temurin) LTS | Windows |
A minimalist JUnit 5 extension to create and destroy Podman containers during tests.
- Conveniently and reliably start containers using
podman
in test suites. - Programmable readiness checks for checking if a container is really ready for use.
- Written in pure Java 21.
- OSGi ready.
- JPMS ready.
- ISC license.
- High-coverage automated test suite.
Test suites often contain integration tests that have dependencies on external servers such as databases. Projects such as testcontainers provide a heavyweight and complex API for starting Docker containers in tests, but no simple, lightweight alternative exists for Podman.
The ervilla
package provides a tiny API and a JUnit 5
extension for succinctly and efficiently creating (and automatically destroying)
Podman containers during tests.
$ mvn clean verify
Annotate your test suite with @ExtendWith(ErvillaExtension.class)
. This
will allow tests to get access to an injected EContainerSupervisorType
instance that can be used to create containers.
ervilla
maintains a persistent store of the containers and pods that it
has created. Each time the ervilla
package is started, it will attempt to
clean up any pods and/or containers that have inadvertently managed to survive
the previous test run. The ervilla
package expects each test suite to
provide a project name; A test run in a given project will only attempt
to clean up containers/pods that are labelled with the same project name.
This is important with regard to concurrency: ervilla
is designed to be
used in sets of projects that may be built in parallel on the same physical
machine during continuous integration runs. By keeping containers strictly
separated by project name, the package avoids accidentally trying to clean up
the containers that may be created by the other project's test suite running
in parallel with the current project.
It can be useful to have tests simply disable themselves rather than running
and failing if they are executed on a system that doesn't have a podman
executable. Both the name of the podman
executable and a flag
that disables tests if podman
isn't supported can be specified in an
@ErvillaConfiguration
annotation placed on the test class.
An example test from the test suite:
@ExtendWith(ErvillaExtension.class)
@ErvillaConfiguration(
projectName = "com.io7m.example"
podmanExecutable = "podman",
disabledIfUnsupported = true
)
public final class ErvillaExtensionContainerPerTest
{
private EContainerType container;
@BeforeEach
public void setup(
final EContainerSupervisorType supervisor)
throws Exception
{
this.container =
supervisor.start(
EContainerSpec.builder(
"quay.io",
"io7mcom/idstore",
"1.1.0"
)
.setImageHash(
"sha256:e77ad1f7f606a42a6bb0bbc885030a647fa4b90c15d47d5b32f43f1c98475f6e")
.addPublishPort(new EPortPublish(
new EPortAddressType.All(),
51000,
51000,
TCP
))
.addPublishPort(new EPortPublish(
new EPortAddressType.All(),
51001,
51001,
TCP
))
.addEnvironmentVariable("ENV_0", "x")
.addEnvironmentVariable("ENV_1", "y")
.addEnvironmentVariable("ENV_2", "z")
.addArgument("help")
.addArgument("version")
.build()
);
}
@Test
public void test0()
{
assertTrue(this.container.name().startsWith("ERVILLA-"));
}
@Test
public void test1()
{
assertTrue(this.container.name().startsWith("ERVILLA-"));
}
@Test
public void test2()
{
assertTrue(this.container.name().startsWith("ERVILLA-"));
}
}
An injected supervisor instance has one of the following scope values:
PER_SUITE
PER_CLASS
PER_TEST
Containers created from a supervisor with PER_SUITE
scope will be destroyed
at the end of the entire test suite run.
Containers created from a supervisor with PER_CLASS
scope will be destroyed
at the end of the containing test class execution.
Containers created from a supervisor with PER_TEST
scope will be destroyed
at the end of each test method.
To get a PER_SUITE
scoped supervisor, annotate the injected supervisor
parameter with @ErvillaCloseAfterSuite
.
To get a PER_CLASS
scoped supervisor, annotate the injected supervisor
parameter with @ErvillaCloseAfterClass
.
To get a PER_TEST
scoped supervisor, do not annotate the injected
supervisor.
@ExtendWith(ErvillaExtension.class)
@ErvillaConfiguration(disabledIfUnsupported = true)
public final class ErvillaExtensionCloseAfterAllTest
{
private static EContainerType CONTAINER;
@BeforeAll
public static void beforeAll(
final @ErvillaCloseAfterClass EContainerSupervisorType supervisor)
throws Exception
{
CONTAINER =
supervisor.start(
EContainerSpec.builder(
"quay.io",
"io7mcom/idstore",
"1.1.0"
)
.addPublishPort(new EPortPublish(
new EPortAddressType.All(),
51000,
51000,
TCP
))
.addPublishPort(new EPortPublish(
new EPortAddressType.All(),
51001,
51001,
TCP
))
.addEnvironmentVariable("ENV_0", "x")
.addEnvironmentVariable("ENV_1", "y")
.addEnvironmentVariable("ENV_2", "z")
.addArgument("help")
.addArgument("version")
.build()
);
}
@Test
public void test0()
{
}
@Test
public void test1()
{
}
@Test
public void test2()
{
}
}
The container is created once in the @BeforeAll
method and assigned to
a static field CONTAINER
. Each of test0()
, test1()
, test2()
can
use this container instance, and the instance itself will be destroyed when
the test class completes.
It is common, in test suites, to instantiate heavyweight containers such as databases just once and then reuse those containers throughout the entire test suite. The reuse of a container (with appropriate code to drop and recreate a database each time) can turn a ten minute test suite execution into a one minute execution.
The following is a relatively safe way to achieve this:
class TestServices {
private static EContainerType DATABASE;
public static void resetDatabase(
final EContainerType container)
{
// Database-specific code here, to drop and recreate databases...
...
...
}
public static EContainerType createDatabase(
final EContainerSupervisorType supervisor)
{
// Database-specific code here, to create database containers...
...
...
}
public static EContainerType database(
final EContainerSupervisorType supervisor)
{
if (DATABASE == null) {
DATABASE = createDatabase(supervisor);
}
return DATABASE;
}
}
@ExtendWith({ErvillaExtension.class})
@ErvillaConfiguration(projectName = "com.io7m.example", disabledIfUnsupported = true)
class Test0 {
private static EContainerType DATABASE;
@BeforeAll
public static void setupOnce(
final @ErvillaCloseAfterSuite EContainerSupervisorType containers)
throws Exception
{
DATABASE = TestServices.database(supervisor);
TestServices.resetDatabase(DATABASE);
}
@Test
public void test0()
{
// Use database here
}
@Test
public void test1()
{
// Use database here
}
@Test
public void test2()
{
// Use database here
}
}
@ExtendWith({ErvillaExtension.class})
@ErvillaConfiguration(projectName = "com.io7m.example", disabledIfUnsupported = true)
class Test1 {
private static EContainerType DATABASE;
@BeforeAll
public static void setupOnce(
final @ErvillaCloseAfterSuite EContainerSupervisorType containers)
throws Exception
{
DATABASE = TestServices.database(supervisor);
TestServices.resetDatabase(DATABASE);
}
@Test
public void test0()
{
// Use database here
}
@Test
public void test1()
{
// Use database here
}
@Test
public void test2()
{
// Use database here
}
}
Assuming that resetDatabase
can drop and recreate the container's
database, and createDatabase
does the initial creation of the
database container, the above Test1
and Test0
classes will reuse
the exact same database container instance.
It is possible to provide implementations of the EReadyCheckType
interface for each container. A readiness check is a piece of code that
is executed in a loop until it returns a positive response, and is responsible
for performing some kind of image-specific check to determine if a container
is actually ready for use.
By default, with no readiness checks provided for a container, the
system will only consider a container to be ready for use when the
underlying podman
executable says that the container is in state Up
.
However, for some images, the container being in the Up
state doesn't
necessarily mean that the container is actually ready for use, and this
is where readiness checks should be used.
A good example of this is the images for PostgreSQL.
The PostgreSQL server takes a few seconds to start up while it loads various
bits of configuration and database state from the disk. This means that,
despite the container being in the Up
state, the database won't actually be
ready to service database requests for at least a few seconds after startup.
The EPgReadyCheck
class is a PostgreSQL-specific
readiness check that repeatedly tries to open a JDBC connection to a created
PostgreSQL container, and the system won't consider the container to be
ready for use until a JDBC connection succeeds.
Other readiness checks exist, such as checks to attempt to connect to a bound TCP/IP socket on the container.