-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
1st draft for review #1
base: master
Are you sure you want to change the base?
Conversation
```java | ||
// Building a simple container object not linked to any test framework | ||
final Container container = Container.builder() | ||
.withImage("alpine:3.5") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since image
is a mandatory thing maybe we should require it as a parameter of builder()
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, makes sense.
public class SimpleJUnit4Test { | ||
|
||
@Rule | ||
public ContainerTestRule alpine = ContainerTestRule.builder() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do it this way, we will have to duplicate each and every withXXX
method. What if we use something like I proposed with Networks? i.e.:
Container.builder()
.withSomething()
.build(asJUnit4Rule());
Container.builder()
.withSomething()
.build(asJUnit5Rule());
Container.builder()
.withSomething()
.build(); // aka GenericContainer
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about composing it the other way around?
Something like:
ContainerTestRule.builder(
Container.builder()
.withSomething()
).build();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kiview I thought about it as well, but (compared to my example) it adds more code around it without providing any visible benefits IMO. Also, if .build()
accepts an interface, it will be possible to easily discover all available adapters like JUnit4/5 and others
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 for the discoverability, makes sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually as-is we don't have to duplicate the with
methods... If you look, the junit.ContainerTestRuleBuilder
class extends BaseContainerBuilder
, which does the generic self-typed-return thing we currently do in Testcontainers 1.x. (This helps allow for a fluent interface).
containercore.ContainerBuilder
also uses this same base class as well.
As such, we don't need to duplicate the methods.
The downside is that self-typed-returns are a bit confusing, but at least they're isolated to the builder classes now. I'm not sure how you feel about keeping this technique in use, though.
@bsideup I'm all for the approach you suggest if we don't go for this model above. The only reason I didn't go for that straight away is a slight discoverability worry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TBH I myself don't really understand the type hack 😅
It's for keeping the this type when calling methods from the parent class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe, yes, it's so SomeSubclass::withSomeMethodFromParent
returns as SomeSubclass
rather than the parent class.
I'll consider this as a vote against 😆
|
||
@Test | ||
public void doNothing() { | ||
alpine.exec("date"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
re exec
and others. Since the number of such "helpers" will grow, how about introducing a concept of "recipes"? i.e. ExecRecipe
, GetFileRecipe
, etc?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think in general we want a OO abstraction of docker-java, so I assume we will have all basic docker API methods available, like:
- cp
- exec
- diff
- ...
I always wonder how much to deviate from the known Docker API or if it would be kind of enough to conveniently expose these methods.
Of course we should name these methods in a Java idiomatic way.
Your example recipes are now basically Docker API methods, can you image more complex helper methods?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that this is the goal actually :D The goal is to have a high-level API to start and manipulate the containers, and cp/exec/diff/etc are too low level for that
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is a good thing to discuss. The way I see it, the library should be 'narrow but deep':
Narrow: we don't need to cover every possible method of the docker API (probably >50%, but definitely <100%)
Deep: we should aim to do as much as possible for the user to hide the 'accidental complexity' of docker - e.g. all the things around file mounting, copying, and network port mapping.
By hiding some of this complexity we do reduce the flexibility of the docker API, but this is unavoidable.
There might be people who want a 100% wide abstraction, but TBH I think these people need the finer-grained, lower-level tools that docker-java provides, so we're OK to avoid this area.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bsideup I don't think cp
and exec
are too low level per se, since both of your examples are basically renamed calls to these methods, aren't they ? ;)
There might be more convenient API composed on top of the basic APIs, but I don't see any pain in exposing them in a public API, still more convenient to use in a OO manner then docker-java.
```java | ||
final Container containerWithPlugin = Container.builder() | ||
.withImage("alpine:3.5") | ||
.withPlugin(new FakeTimePlugin("2010-01-01 00:00:00")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe extension
would be a better name? So that we extend container by composing different extensions, but the system itself is not pluggable, at least not with this API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think both terms are fine and apply here, the Wikipedia definition of plugin:
In computing, a plug-in (or plugin, add-in, addin, add-on, addon, or extension) is a software component that adds a specific feature to an existing computer program.
We add functionality or behavior to the container, but we could also extend the existing feature set ;)
There is also the plugin definition by Martin Fowler in Patterns of Enterprise Application Architecture which would fit to this implementation I think (basically meaning DI).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't mind; 'plugin' seemed more natural to me.
One thing which occurred to me is that this isn't essential functionality from day one (it's a new, rather than existing, feature).
Perhaps let's not spend too much time worrying about the details for now, as long as we can agree we'd like this concept to exist in some form 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, since the fluent builder API would allow us to introduce such functionality without any breaking changes we can simply defer the discussion 🙂
.build(); | ||
|
||
// later, while container is running | ||
fakeTime.setTime("2011-01-01 00:00:00"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would make it hard to reuse. How about containerWithPlugin.using(fakeTime).setTime(...)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference, since we still need the reference to the field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
containerFoo.using(fakeTime).setTime(...)
containerBar.using(fakeTime).setTime(...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice, might be problematic for stateful Plugins (which might exist).
Or maybe we could have something like:
final Container containerWithPlugin = Container.builder()
.withImage("alpine:3.5")
.withPlugin(FakeTimePlugin.class);
containerWithPlugin.using(FakeTimePlugin.class).setTime(...);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stateful plugins is a pain. Even if plugin wants to store the state, it's better if it will use something like https://docs.oracle.com/javase/7/docs/api/java/util/WeakHashMap.html
|
||
--- | ||
|
||
##Runtime `Container` vs define-time `ContainerBuilder` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should take a look at https://github.com/sundrio/sundrio/blob/master/annotations/dsl/readme.md for this. It's being used in fabric8's Docker client and seems to be very powerful DSL generator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 Thanks for the tip - will have a look
/** | ||
* TODO: Javadocs | ||
*/ | ||
public class Container { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to suggest making this class AutoClosable
. The interface can simply invoke stop
and should override the method to remove the Exception
as none is thrown.
This allows for a very comfortable usage of containers such as:
try(Container container = new MyContainer()) {
container.start();
// do something with container..
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@raphw 👍 good catch! In fact, we already have it in TestContainers and it proved to be useful :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 absolutely - a small detail that adds a lot of benefit. Thanks!
* making sure all fluent setters are consistent | ||
* adding a separation between definition-time concerns and 'run-time' concerns | ||
* add a plugin model to allow *composition* to be used in addition to *inheritance* | ||
* **Continue to support specialized container types via specialized classes** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we already started the discussion in Slack - we don't want to have Docker-Compose support as part of containercore, correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth bringing up, and we should really add to this doc as it's a 'future of testcontainers' question. In brief, I think I'd see it as:
Testcontainers / Test framework integrations (e.g. JUnit4/5, Spock, etc)
\ Specialised container support (e.g. database, selenium, compose, etc)
Containercore -- High level OO docker API
I'd hope there's nothing in containercore that has to be aware of docker-compose.
Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we decide to exclude docker-compose from core (which makes sense), we should IMO provide a compose-core module, which itself is decoupled from testcontainers. There are really a lot of use cases for the high level docker-compose support, even outside of test frameworks. And if I think about it, not all of the specialized containers seem to be testcontainers specific.
So maybe it would be more like this?
Testcontainers -- Test framework integrations (e.g. JUnit4/5, Spock, etc)
Containercore -- High level OO docker API
Containercore-modules -- Specialised container support (e.g. database, selenium, compose, etc)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point - trying out a couple of ways to achieve this now.
```java | ||
final Container containerWithPlugin = Container.builder() | ||
.withImage("alpine:3.5") | ||
.withPlugin(new FakeTimePlugin("2010-01-01 00:00:00")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think both terms are fine and apply here, the Wikipedia definition of plugin:
In computing, a plug-in (or plugin, add-in, addin, add-on, addon, or extension) is a software component that adds a specific feature to an existing computer program.
We add functionality or behavior to the container, but we could also extend the existing feature set ;)
There is also the plugin definition by Martin Fowler in Patterns of Enterprise Application Architecture which would fit to this implementation I think (basically meaning DI).
.build(); | ||
|
||
// later, while container is running | ||
fakeTime.setTime("2011-01-01 00:00:00"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the difference, since we still need the reference to the field?
### Implementation | ||
|
||
* `Container` holds configuration data in fields, plus methods for 'run-time' interactions such as `start`, `stop` , `exec`, etc. | ||
* `ContainerBuilder` (and its parent class `BaseContainerBuilder`) exposes the define-time `withXYZ` fluent setter API |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you guys think about the immutable builder? With the current proposal builder is mutable and it could lead to some surprising behavior. For example, I want to have 2 PosgtreSQL containers who shares some common settings:
final ContainerBuilder commonBuilder = PostgreSQLContainer.builder().withImage("posgtresql:9.6");
final Container pg1 = commonBuilder.withExposedPort(1234).build();
final Container pg2 = commonBuilder.withExposedPort(2345).build();
But this code would not work as expected because of mutable nature of builders.
I think immutability is always a good default for the libraries. In testcontainers we don't need high-performant builders, so extra allocations is not a problem.
The new proposal doesn't take into account tests with multiple dependent containers. With the current implementation, I had some issues and had to use some hacks. testcontainers currently expose api only for test<->container interoperability, but there is no convenient container<->container interface. And I think one of the most exciting use case for testcontainers is tests with multiple dependent containers. I suggest adding some methods to |
@LMnet actually, we have Networks support in TestContainers for the container <-> container communication: |
@bsideup Yes, I know about networks support. But it looks like container <-> container communication features are not given enough attention. For example, to obtain an IP for a container inside a network I need to do something like this: container.getContainerInfo().getNetworkSettings().getNetworks().get(myNetworkId).getIpAddress() Looks a bit verbose for such simple operation. |
@LMnet why would you do it at all? Just use a network alias (i.e. |
@bsideup It was just an example of API usage. Another real-life example. In my tests, I start an HTTP mock on my host machine (from the test code). The application is inside a container, and this container is inside a custom network. The application should be able to do HTTP request to the mock. To link mock with the application I need to get network's gateway address inside the container - and it would host's address. So, to get a gateway address I need something like this: container.getContainerInfo().getNetworkSettings().getNetworks().get(myNetworkId).getGateway() ATM I don't know any better way to do this. |
As requested, I've converted this into a PR for easier review.