Testcontainers-java: Controlling execution order of JUnit 5 extension relative to other extensions not possible

Created on 18 Dec 2018  路  5Comments  路  Source: testcontainers/testcontainers-java

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

This issue is not limited to Spring, but rather an issue as soon as multiple extensions are used at the same time and a certain ordering is required.

Context

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.

Possible solution

I believe making TestcontainersExtension public is all that's needed for users to be able to explicitly define the order of multiple extensions.


Based on https://github.com/testcontainers/testcontainers-java/pull/887#issuecomment-447815105 and https://github.com/testcontainers/testcontainers-java/pull/887#issuecomment-447818414.

resolutiopr-submitted typbug

All 5 comments

Probably related to #1019.

I created a reproducer in https://github.com/jrehwaldt/testcontainers-gh1017

The issue when using Kotlin is not the annotation order (maybe in Java it is as per JUnit documentation?), but instead that Spring will load the context very early in case of bean injection into the constructor.

Doesn't work:

@SpringBootApplication
class Application

@Component
class SomeSimpleBean

@Testcontainers // Has to run before SpringBootTest (SpringExtension) is run,
@SpringBootTest // but because of <constructorInjectedBean> below this doesn't happen.
@ContextConfiguration(initializers = [TestcontainersIssueGh1017ConstructorInjection.DynamoDbAwareInitializer::class])
class TestcontainersIssueGh1017ConstructorInjection(
    @Autowired private val constructorInjectedBean: SomeSimpleBean
) {

    companion object {
        @Container
        private val dynamodb = KGenericContainer("richnorth/dynalite:latest")
            .withExposedPorts(4567)
    }

    @Test
    fun `should execute TestcontainerExtension before SpringExtention`() {
        // When we reach here we're fine
    }

    internal class DynamoDbAwareInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {
        override fun initialize(context: ConfigurableApplicationContext) {
            // FIXME Next line will fail with
            // > java.lang.IllegalStateException: Mapped port can only be obtained after the container is started
            val dynamodbEndpoint = "http://${dynamodb.containerIpAddress}:${dynamodb.getMappedPort(4567)}"
            TestPropertyValues.of(
                "aws.dynamodb.endpoint: $dynamodbEndpoint",
                "aws.dynamodb.create-tables: true"
            ).applyTo(context)
        }
    }
}

Works as expected:

@Testcontainers // Runs before SpringBootTest as expected
@SpringBootTest // when using field injection.
@ContextConfiguration(initializers = [TestcontainersIssueGh1017FieldInjection.DynamoDbAwareInitializer::class])
class TestcontainersIssueGh1017FieldInjection {

    companion object {
        @Container
        private val dynamodb = KGenericContainer("richnorth/dynalite:latest")
            .withExposedPorts(4567)
    }

    @Autowired
    private lateinit var fieldInjectedBean: SomeSimpleBean

    @Test
    fun `should execute TestcontainerExtension before SpringExtention`() {
        // When we reach here we're fine
    }

    internal class DynamoDbAwareInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {
        override fun initialize(context: ConfigurableApplicationContext) {
            val dynamodbEndpoint = "http://${dynamodb.containerIpAddress}:${dynamodb.getMappedPort(4567)}"
            Thread.sleep(1000)
            TestPropertyValues.of(
                "aws.dynamodb.endpoint: $dynamodbEndpoint"
            ).applyTo(context)
        }
    }
}

Changing the annotation order between @Testcontainers and @SpringBootTest correctly changes the invocation order of both extensions in Kotlin.

All four tests are green when running against #1020.

Thanks a lot for checking against #1020.
Let's try to merge it soon :slightly_smiling_face:

@kiview @michael-simons I'm trying to write a new JUnit Jupiter extension that builds on top of the Testcontainers extension. The original solution proposed in this issue (make TestcontainersExtension a public class) is exactly I need, because then I can do:

@Target(TYPE)
@Retention(RUNTIME)
@ExtendWith({TestContainerExtension.class, MyExtension.class})
public @interface MyExt { }

I'm trying to follow the various issues linked in this issue to find out why the TestContainersExtension class was never made public, but didn't find anything. Is this option still on the table?

@aguibert asked right question, @kiview @michael-simons why aren't TestcontainersExtension and related classes public?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

andredasilvapinto picture andredasilvapinto  路  3Comments

rnorth picture rnorth  路  3Comments

McKratt picture McKratt  路  4Comments

naderghanbari picture naderghanbari  路  3Comments

chomhanks picture chomhanks  路  3Comments