Skip to content

Instantly share code, notes, and snippets.

@rnorth
Last active July 18, 2017 15:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rnorth/6a2f9f6825ff50cfabdd92ed820768f8 to your computer and use it in GitHub Desktop.
Save rnorth/6a2f9f6825ff50cfabdd92ed820768f8 to your computer and use it in GitHub Desktop.
2.0 API refactor thoughts

Testcontainers v2.0 API changes - idea 1

This is a rough sketch of how a revised API for Testcontainers could look - don't read too much into it.

Key goals:

  • How it works should be simple and obvious for the benefit of users and contributors
  • Don't lose too much in-IDE 'discoverability' of the API when compared with the current implementation
  • Break dependency on JUnit to allow usage with other testing frameworks
  • Have separate APIs for configuring containers and using a running container
  • Allow composition to be used to add additional 'value-adding' config/runtime modifiers to be applied to containers
/*
* Usage examples (the important bit). Please cut some slack on syntax etc.
*/
class JUnit4Usage {
@Rule
public ContainerRule redis = Container.builder()
// as core features 'image' and 'volume' are baked in to the Container class
// (there will be many other such features we deem to be 'core')
.image(named("redis:4.0"))
.volume(mount("/foo", "/bar", "ro"))
// 'buildAs' actually provides a Container wrapped in a ContainerRule, for JUnit 4 usage
.buildAs(RULE);
@Test
public void testSomething() {
// invoking a runtime action on the running container.
// Some indirection via 'container()' call due to ContainerRule actually being a wrapper
redis.container().exec("echo 'Hello world'");
}
}
@RunWith(TestcontainersJUnit5Runner.class)
class JUnit5Usage {
// JUnit 5 benefits from having a cleaner API.
public Container redis = Container.builder()
.image(named("redis:4.0"))
.build();
@Test
public void testSomething() {
redis.exec("echo 'Hello world'");
}
}
class JUnit4PluginUsage {
/*
* A 'plugin' would be anything that is not a core feature that needs to (a) influence container config/creation,
* (b) perform tasks at runtime
*/
@Rule
public ContainerRule redis = Container.builder()
.image(named("redis:4.0"))
.plugin(fakeTime(new Date(0))) // <-- example 'faketime' plugin - applying a setting which affects config
.buildAs(RULE);
@Test
public void testSomething() {
redis.container()
.plugin(FakeTimePlugin.class) // obtaining a ref to a plugin instance at runtime, to do something with it
.changeTime(new Date(1000));
}
}
/*
* INTERNALS
*/
@Builder
public class Container {
private Image image;
@Singular private List<Volume> volumes;
@Singular private Set<Plugin> plugins = new TreeSet<>(comparingInt(Plugin::priority));
public class ContainerBuilder {
@SneakyThrows
public <T> T buildAs(Class<T> toType) {
return toType.getConstructor(Container.class).newInstance(this.build());
}
}
private ContainerConfig configure() {
plugins.forEach(it -> it.beforePullingImage(this));
// resolve image and pull
plugins.forEach(it -> it.afterPullingImage(this));
// create a container config object
ContainerConfig config = new ContainerConfig();
plugins.forEach(it -> it.beforeCoreConfiguration(this, config));
// do normal config for core features - e.g. setting up volume mounts, networks, etc
// ....
// then return
return config;
}
public void start() {
// do normal setup, configure(), and start
ContainerConfig config = configure();
plugins.forEach(it -> it.beforeCreate(this, config));
com.github.dockerjava.api.model.Container dockerJavaContainer = // create it
plugins.forEach(it -> it.beforeStart(this, dockerJavaContainer));
// start it
// ...
plugins.forEach(it -> it.afterStart(this, dockerJavaContainer));
}
public void stop() {
// do container shutdown
}
/*
* 'exec' is an example of a core feature that can be used at runtime
*/
public int exec(String command) {
// exec in the container
return 0;
}
/*
* Obtain a reference to a plugin that has been used on this container, to enable runtime actions to be performed
*/
public <T> T plugin(Class<T> pluginClass) {
return (T) plugins.stream()
.filter(it -> it.getClass().equals(pluginClass))
.findFirst()
.get();
}
}
@RequiredArgsConstructor
class Image {
private final String imageName;
}
@RequiredArgsConstructor
class Volume {
private final String host;
private final String container;
private final String mode;
}
/*
* This class provides static factories for core features. It is provided to aid discoverability;
* its methods should be used via static import(s)
*/
@UtilityClass
class CoreConfigurationElements {
public static Image named(String imageName) {
return new Image(imageName);
}
public static Volume mount(String hostPath, String containerPath, String mode) {
return new Volume(hostPath, containerPath, mode);
}
}
/*
* A plugin has the ability to take control at any point during container creation/startup
*/
interface Plugin {
default void beforePullingImage(Container container) {}
default void afterPullingImage(Container container) {}
default void beforeCoreConfiguration(Container container, ContainerConfig config) {}
default void beforeCreate(Container container, ContainerConfig config) {}
default void beforeStart(Container container, com.github.dockerjava.api.model.Container dockerJavaContainer) {}
default void afterStart(Container container, com.github.dockerjava.api.model.Container dockerJavaContainer) {}
default int priority() { return 100; }
}
/*
* A made-up example of a plugin. This example shows that a plugin can amend ContainerConfiguration before it is
* created, and also provides a method that can be invoked from test code while the container is running.
*/
@NoArgsConstructor
class FakeTimePlugin implements Plugin {
private Date fakeTime;
public FakeTimePlugin(Date fakeTime) {
this.fakeTime = fakeTime;
}
public static FakeTimePlugin fakeTime(Date fakeTime) {
return new FakeTimePlugin(fakeTime);
}
@Override
public void beforeCreate(Container container, ContainerConfig config) {
// do necessary installation/mounting; amend cmd
// hold onto Container for any future manipulation
}
public void changeTime(Date newFakeTime) {
this.fakeTime = newFakeTime;
// do something with Container
}
}
/*
* Simplified example of a shim that wraps a Container so that it can be used as a Rule.
*/
class ContainerRule extends ExternalResource {
public static Class<ContainerRule> RULE = ContainerRule.class;
private final Container container;
ContainerRule(Container container) {
this.container = container;
}
@Override
protected void before() throws Throwable {
container.start();
}
@Override
protected void after() {
container.stop();
}
public Container container() {
return container;
}
}
/*
* ignore the implementation of this - this is based on JUnit 4 libs
*/
class TestcontainersJUnit5Runner extends Runner {
...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment