Skip to content
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

JUnit 5 extension #887

Merged
merged 41 commits into from
Nov 2, 2018
Merged

JUnit 5 extension #887

merged 41 commits into from
Nov 2, 2018

Conversation

britter
Copy link
Contributor

@britter britter commented Sep 28, 2018

This is related to #636 but takes a different approach: instead of upgrading testcontainers core to JUnit 5 APIs, this introduces a new module containing a JUnit 5 extension that uses testcontainers core.

Static fields are shared among test methods (started only once and stopped at the end), while instance fields are started and stopped for every method. This approach has the advantage that it is relatively easy to implement. However, the downside of this is, that shared containers can't be used inside of @Nested test cases since these must not be declared as static inner classes and hence can not have static fields.

Feedback welcome @kiview, @sormuras, @nicolaiparlog, @marcphilipp

@kiview
Copy link
Member

kiview commented Sep 28, 2018

Hey @britter, great that you've contributed the JUnit5 extension, I told you it's not too much work 😉
And cool approach similar to the Spock implementation, let's see what the others will think about this.

I'll do the review later this weekend 🙂

@bsideup
Copy link
Member

bsideup commented Sep 29, 2018

Hi @britter! Thanks for contributing your vision of JUnit 5 support 👍
I'm totally okay to replace my PR with yours :) Could you please migrate the test cases tho? I've tried to reflect most of the use cases there, and (when I was thinking about the implementation) some use cases were not possible to cover with an extension, at least with the annotation-based one, but maybe I'm wrong :)

@britter
Copy link
Contributor Author

britter commented Sep 29, 2018

Hi @bsideup,

sure thing. Which test cases do you mean? Those you added in #636?
The Codacy analysis fails but the failures seem to be false positives. Can you confirm?

Copy link

@nipafx nipafx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I was @'ed, I figured I'd give a quick review. I know little about Testcontainers (except that once they have proper JUnit 5 integration and no longer rely on JUnit 4, I will start using them immediately) and so focused on the Jupiter extension. Straightforward and well implemented. 👍

* {@code @Testcontainers} is a JUnit Jupiter extension to activate automatic
* startup and stop of test containers used in a test case.
*
* <p>The test containers extension finds all fields of typ
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "typ"

class TestcontainersExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback, AfterEachCallback {

@Override
public void beforeAll(final ExtensionContext context) throws IllegalAccessException {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could reduce code duplication between beforeAll and afterAll by having a method forEachSharedContainer(Consumer<Field>) that does the same thing as this method, but instead of startContainer calls Consumer::consume with the field.

}

@Override
public void beforeEach(final ExtensionContext context) throws IllegalAccessException {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as for beforeAll.

import static org.junit.jupiter.api.Assertions.assertEquals;

@Testcontainers
class ComposeContainerIT {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how Testcontainers in general or these new tests in particular are structured, so my point may be moot, but here it goes: I moved away from using test class names to distinguish unit and integration test. Instead I apply Jupiter tags (e.g. with @Tag("integration")) and configure the build tool to filter by tags instead of file names. If you're interested in that, let me know - I have more than these two cents to give. 😉

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We build Testcontainers using Gradle and currently don't have dedicated source sets for Unit- and Integration-Tests. I would assume if we would retstructure our testing phases, we would use two distinct source sets for Unit- and Integration-Tests, so we can use it for all modules.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you could use tags and define two separate test tasks.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would avoid IT suffix (a rudiment from Maven)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think actually just copied over from the Spock tests 🙂

@rnorth
Copy link
Member

rnorth commented Sep 30, 2018

@britter thanks for raising this! I haven't had a chance to review/play with this yet, but re the Codacy warnings: yes, please consider these false positives. There's nothing there that would worry me for test-scoped code.


/**
* {@code @Testcontainers} is a JUnit Jupiter extension to activate automatic
* startup and stop of test containers used in a test case.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would omit the 'test' from 'test container' here.

import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* This test verifies, that setup and cleanup of containers works correctly.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder right know, if this comment is still true for the existing Spock tests structure. Either way, this is kind of hackish and I think we should refactor this test in the Spock module and don't want to duplicate it for this new module :D

Copy link
Member

@kiview kiview left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @bsideup is referring to the inheritance and abstract class tests:
https://github.com/testcontainers/testcontainers-java/pull/636/files#diff-d6b833ecb1659337d0093eab00102635

I'm very happy with the PR, since the integration surface is super slim and the API is the same than our Spock implementation.

One thing to keep in mind is, that VncRecording will not work right now. But we can add the support in a later PR.

@britter
Copy link
Contributor Author

britter commented Oct 1, 2018

I'm doing another iteration. @nicolaiparlog has pointed me to the TestInstancePostProcessor extension point in JUnit Jupiter. This way I can implement shared containers as instance fields instead of static fields. This way the @Nested use case can also be implemented.

I'll also adapt the inheritance test case making sure that this use case is also supported!

@britter
Copy link
Contributor Author

britter commented Oct 1, 2018

After another iteration I came up with a solution that is based on TestInstancePostProcessor. I've not pushed my solution, it has other draw backs which I'd like to outline below. I think we need to decide which approach we want to use and live with the drawbacks:

Solution 1 uses static fields to enable shared containers. This way shared containers are bound the the test class. The problem with this approach is, that shared containers can't be used inside nested test cases, because nested test cases must not be declared as static classes and can therefore not have static fields.

Solution 2 uses the TestInstancePostProcessor extension point to create shared containers. Shared containers can be declared as instance fields, so they are bound to the test instance rather then to the test class. The problem with this is, that containers can only be shared if the test is annotated with @TestInstance(Lifecycle.PER_CLASS) because otherwise it would be recreated every time the test class is instantiated. The the drawback here is, that users have to be aware of this and that they are forced to change the test instance lifecycle for shared containers to work. On the other hand with this approach shared containers also work in nested test cases.

I currently don't know which solution we should implement... So I'm happy for every feedback.

@kiview
Copy link
Member

kiview commented Oct 2, 2018

I think nested tests is a super convenient and powerful feature, however I'm not sure how many users will actually use them or know about this feature? Is it very prominent in JUnit5?

I have no real problem with **Solution 2 ** if we document it accordingly and would prefer it in order to support nested tests.

@rnorth @bsideup WDYT?

Copy link
Contributor

@marcphilipp marcphilipp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding solution 2: There's no way around the instance variable being instantiated twice. You could store the first one in the extension context of the nested test class and replace it using a TestInstancePostProcessor. However, I would consider that a hack. 😉

Thus, I'm in favor of using static fields and adding documentation that those won't work in nested classes.

*
* <p>The test containers extension finds all fields of typ
* {@link org.testcontainers.lifecycle.Startable} and calls their container
* lifecylce methods. Containers declared as static fields will be shared
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: "lifecylce"

@Override
public void beforeAll(final ExtensionContext context) throws IllegalAccessException {
Class<?> testClass = context.getRequiredTestClass();
for (final Field field : testClass.getDeclaredFields()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should take into account fields declared in super classes, shouldn't it? Same for the other methods in this class.

@Override
public void afterEach(final ExtensionContext context) throws IllegalAccessException {
Object testInstance = context.getRequiredTestInstance();
for (Field field : testInstance.getClass().getDeclaredFields()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you could store the started containers in the ExtensionContext and retrieve them from there. You could even store CloseableResource implementations and let Jupiter take care of stopping the containers.

import static org.junit.jupiter.api.Assertions.assertEquals;

@Testcontainers
class ComposeContainerIT {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you could use tags and define two separate test tasks.

@britter britter changed the title [WIP] JUnit 5 extension JUnit 5 extension Oct 25, 2018
@testcontainers testcontainers deleted a comment Oct 25, 2018
@kiview
Copy link
Member

kiview commented Oct 26, 2018

Great, can you have a final look, @rnorth @bsideup?

@rnorth
Copy link
Member

rnorth commented Oct 27, 2018

This looks amazing - thank you all for contributing and refining this! I only had one minor comment on the docs; this is already a well polished PR by the time I've got to it 😄.

I'd like to have a little bit more of a play with this tonight, but I'll be looking forward to us merging and releasing this ASAP.

Thank you 🙇

import static org.junit.jupiter.api.Assertions.assertTrue;

@Testcontainers
class TestcontainersNestedRestaredContainerIT {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiny typo!

Suggested change
class TestcontainersNestedRestaredContainerIT {
class TestcontainersNestedRestartedContainerIT {

rnorth and others added 2 commits October 28, 2018 08:45
Co-Authored-By: britter <beneritter@gmail.com>
@testcontainers testcontainers deleted a comment Oct 28, 2018
@kiview kiview added this to the next milestone Oct 29, 2018
@bsideup bsideup mentioned this pull request Oct 29, 2018
store.put(TEST_INSTANCE, testInstance);

findSharedContainers(testInstance)
.forEach(adapter -> store.getOrComputeIfAbsent(adapter.key, k -> adapter.start()));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to use field instead of getter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The StoreAdapter class is a private class inside of TestcontainersExtension. So all fields are visible from TestcontainersExtension and there is no gain from an information hiding perspective if we add getters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem I see is that it is inconsistent with the rest of our code base because we always use getters :)

}

private static Predicate<Field> isContainer() {
return field -> Startable.class.isAssignableFrom(field.getType()) && AnnotationSupport.isAnnotated(field, Container.class);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would return any field annotated with @Container and throw and error if it is not Startable, otherwise it will simply ignore it

@sormuras
Copy link

sormuras commented Oct 29, 2018

I know, this PR is in a late and almost finished state -- but I promised @britter to spike a different way to handle shared containers, which is also thread safe. See https://github.com/junit-team/junit5-samples/compare/singleton-extension for a work-in-progress implementation using a StringBuilder as a shared resource.

Transferred to this container context, the test class could read like:

@ExtendWith(SingletonExtension.class)
class SingletonExtensionTests {

	@Test
	void test1(@Singleton(MySQL123.class) MySQLContainer shared) {}

	@Test
	void test2(@Singleton(MySQL123.class) MySQLContainer shared) {}

	@Test
	void test3(@New(MySQL123.class) MySQLContainer fresh) {}

}

The MySQL123 class is used as resource factory:

public class MySQL123 implements SingletonExtension.Resource<MySQLContainer> {

	private final MySQLContainer container = new MySQLContainer();

	@Override
	public MySQLContainer get() {
		return container;
	}
}

Edit: refactored the implementation and filed junit-team/junit5-samples#87 as a sample for discussion

@kiview kiview merged commit d7c4708 into testcontainers:master Nov 2, 2018
kiview pushed a commit that referenced this pull request Nov 2, 2018
rnorth pushed a commit that referenced this pull request Nov 4, 2018
* Small improvements to Jupiter extension code style

Follow up to #887

* Only raise exception if annotated field is not Startable
@jrehwaldt
Copy link

I am trying to use this extension together with @SpringBootTest. How to ensure testcontainer starts first before executing SpringBootTest extension?

From what I see in the JUnit 5 documentation the annotation order matters. Since Java sorts annotations in bytecode alphabetically Spring will always run first unless it is possible to explicitly import @ExtendWith(TestcontainersExtension.class). Currently, this cannot be done due to TestcontainersExtension being package-private.

Question is: How to ensure testcontainers ordering relative to other extensions with the current extension mechanism?

@bsideup
Copy link
Member

bsideup commented Dec 17, 2018

Hi @jrehwaldt!
Are you sure that you need JUnit 5 extension here? If you need containers for your Spring Boot app, you can easily add it inside a static block, test framework free:
https://testcontainers.gitbook.io/workshop/step-6-adding-redis

@jrehwaldt
Copy link

Thanks. That's exactly what I am currently doing. I got pointed to this new feature and thought I'd give it a shot. Imho, this extension provides test lifecycle support out of the box without copy and pasting start-stop logic in @[Before|After][All|Each] annotations from test to test. Unfortunately, it seems to me the interplay of multiple extensions wasn't part of the spec'ing, which is why I asked here for clarification.

Am I right in assuming the scenario I describe is simply not supported as of now? I believe making TestcontainersExtension public is all that's needed for users to be able to explicitly define the order of multiple extensions.

@marcphilipp
Copy link
Contributor

@jrehwaldt Since this PR is merged, please open a new issue.

@jrehwaldt
Copy link

I did in #1017. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants