ervilla
JVM | Platform | Status |
---|---|---|
OpenJDK (Temurin) Current | Linux | |
OpenJDK (Temurin) LTS | Linux | |
OpenJDK (Temurin) Current | Windows | |
OpenJDK (Temurin) LTS | Windows |
ervilla
A minimalist JUnit 5 extension to create and destroy Podman containers during tests.
Features
Motivation
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.
Building
$ mvn clean verify
Usage
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.
Container Database
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.
Disable Support
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.0.0-beta0013"
)
.setImageHash(
"sha256:c3c679cbda4fc5287743c5a3edc1ffa31babfaf5be6e3b0705f37ee969ff15ec")
.addPublishPort(new EPortPublish(
Optional.empty(),
51000,
51000,
TCP
))
.addPublishPort(new EPortPublish(
Optional.of("[::]"),
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-"));
}
}
Container Scope
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.0.0-beta0013"
)
.addPublishPort(new EPortPublish(
Optional.empty(),
51000,
51000,
TCP
))
.addPublishPort(new EPortPublish(
Optional.of("[::]"),
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.
Container Reuse
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 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);
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);
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.