Skip to content

Mapped port can only be obtained after the container is started when trying to resolve @ConditionalOnProperty #41664

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

Closed
MrSinisterX opened this issue Aug 1, 2024 · 6 comments
Labels
status: declined A suggestion or change that we don't feel we should currently apply

Comments

@MrSinisterX
Copy link

I have an issue related to #40585 . I am using Spring Boot 3.3.2, which should have the fix, however maybe my scenario is slightly different.

I have a bean which has @ConditionalOnProperty("x") and a test containers configuration which uses dynamic property source to inject the exact same proprty "x". What happens is that the conditional tries to resolve the property before the container is started and cannot access the mapped port.

Container configuration

public interface MockAuthServerContainerConfiguration {
  @Container
  MockAuthServerTestContainer MOCK_AUTH_SERVER_TEST_CONTAINER =
      new MockAuthServerTestContainer().withNetwork(Networks.BRIDGE);

  @DynamicPropertySource
  static void setContainerProperties(DynamicPropertyRegistry registry) {
    registry.add(
        "authentication.url",
        () -> String.format(
                "http://localhost:%d/", MOCK_AUTH_SERVER_TEST_CONTAINER.getFirstMappedPort()));
  }
}

Bean configuration

@Configuration
public class AuthenticationBean{

  @Bean
  @ConditionalOnMissingBean
  @ConditionalOnProperty("authentication.url")
  AuthenticationBean authenticationBean() {
             ...
  }
}

Test usage

@Testcontainers
@ImportTestcontainers({MockAuthServerContainerConfiguration .class})

The behavior is that trying to resolve the property in order to determine bean creation happens before the container is started and the port cannot be retrieved: "Mapped port can only be obtained after the container is started"

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 1, 2024
@wilkinsona
Copy link
Member

The scenario here is quite different as you're querying the value of a property in a condition. Conditions are evaluated very early – while the bean factory is being populated and before any beans exist – and this is too early for the infrastructure that automatically starts a container when a property that it provides is used.

If you want to be able to use @ConditionalOnProperty to query a container-provided property, the lifecycle of that container will have to be managed by Testcontainers rather than by the application context. Here's a minimal example of that:

@SpringBootTest
@Testcontainers
class Gh41664ApplicationTests {
	
	@Container
	private static MockAuthServerTestContainer container = new MockAuthServerTestContainer().withNetwork(Networks.BRIDGE);
	
	@DynamicPropertySource
	static void setContainerProperties(DynamicPropertyRegistry registry) {
		registry.add("authentication.url", () -> String.format("http://localhost:%d/", container.getFirstMappedPort()));
	}

	@Test
	void contextLoads() {
	}

}

Given your use of @Testcontainers and @Container above, perhaps this what you were already striving for? Unfortunately, it's not clear with only a few code snippets to look at.

@wilkinsona wilkinsona added the status: waiting-for-feedback We need additional information before we can continue label Aug 1, 2024
@MrSinisterX
Copy link
Author

MrSinisterX commented Aug 1, 2024

The scenario here is quite different as you're querying the value of a property in a condition. Conditions are evaluated very early – while the bean factory is being populated and before any beans exist – and this is too early for the infrastructure that automatically starts a container when a property that it provides is used.

If you want to be able to use @ConditionalOnProperty to query a container-provided property, the lifecycle of that container will have to be managed by Testcontainers rather than by the application context. Here's a minimal example of that:

@SpringBootTest
@Testcontainers
class Gh41664ApplicationTests {
	
	@Container
	private static MockAuthServerTestContainer container = new MockAuthServerTestContainer().withNetwork(Networks.BRIDGE);
	
	@DynamicPropertySource
	static void setContainerProperties(DynamicPropertyRegistry registry) {
		registry.add("authentication.url", () -> String.format("http://localhost:%d/", container.getFirstMappedPort()));
	}

	@Test
	void contextLoads() {
	}

}

Given your use of @Testcontainers and @Container above, perhaps this what you were already striving for? Unfortunately, it's not clear with only a few code snippets to look at.

Yes, you are right. It is working like this. My goal here was other. I wanted to be able to avoid having this container and the DynamicPropertySource declared in all the test classes where this is needed. I wanted to use the approach with the configuration so that I can reuse the same setup everywhere, without explicitly duplicating that code everywhere... but as you mentioned it is not possible.

With other containers that do not influence beans via conditional checking everything is working ok. So, I guess for these scenarios I have to stick with this.

With the @ImportTestContainers approach I was even able to define my own annotation that provides all the setup to the test class.

e.g.

@WithMyContainer which inherits from @ImportTestContainers({ContainerConfig}) and @Testcontainers

Adding @WithMyContainer to any test class would just provide the container to that test class.

Thank you for your answer!

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Aug 1, 2024
@wilkinsona
Copy link
Member

I wanted to be able to avoid having this container and the DynamicPropertySource declared in all the test classes where this is needed

You can use an interface for this:

interface Containers {

	@Container
	static MockAuthServerTestContainer container = new MockAuthServerTestContainer().withNetwork(Networks.BRIDGE);
	
	@DynamicPropertySource
	static void setContainerProperties(DynamicPropertyRegistry registry) {
		registry.add("authentication.url", () -> String.format("http://localhost:%d/", container.getFirstMappedPort()));
	}
	
}

A test class can then implement this interface:

@SpringBootTest
@Testcontainers
class Gh41664ApplicationTests implements Containers {
	
	@Test
	void contextLoads() {
	}

}

This allows the configuration to be shared while also allowing Testcontainers to manage the container lifecycle.

I don't think there's anything that we can do in Spring Boot to improve this so I'll close this one.

@wilkinsona wilkinsona closed this as not planned Won't fix, can't repro, duplicate, stale Aug 1, 2024
@wilkinsona wilkinsona added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged status: feedback-provided Feedback has been provided labels Aug 1, 2024
@MrSinisterX
Copy link
Author

MrSinisterX commented Aug 1, 2024

I wanted to be able to avoid having this container and the DynamicPropertySource declared in all the test classes where this is needed

You can use an interface for this:

interface Containers {

	@Container
	static MockAuthServerTestContainer container = new MockAuthServerTestContainer().withNetwork(Networks.BRIDGE);
	
	@DynamicPropertySource
	static void setContainerProperties(DynamicPropertyRegistry registry) {
		registry.add("authentication.url", () -> String.format("http://localhost:%d/", container.getFirstMappedPort()));
	}
	
}

A test class can then implement this interface:

@SpringBootTest
@Testcontainers
class Gh41664ApplicationTests implements Containers {
	
	@Test
	void contextLoads() {
	}

}

This allows the configuration to be shared while also allowing Testcontainers to manage the container lifecycle.

I don't think there's anything that we can do in Spring Boot to improve this so I'll close this one.

This doesn't seem to be working since static void setContainerProperties(DynamicPropertyRegistry registry) is a static method and when Testcontainers creates a new container for another test class, then the spring context doesn't get updated with the new port number, as this only gets executed once. The services are pointing to the "old" container that was created first.

@wilkinsona
Copy link
Member

I would guess that's due to context caching and that the affected tests have otherwise identical configuration. You may need to use @DirtiesContext so that each test gets it own context and the lifecycles will then align. I think this is another variant of #38237.

@MrSinisterX
Copy link
Author

I would guess that's due to context caching and that the affected tests have otherwise identical configuration. You may need to use @DirtiesContext so that each test gets it own context and the lifecycles will then align. I think this is another variant of #38237.

Using @DirtiesContext works, however using spring.test.context.cache.maxSize=1 as you used in 138307c doesn't.

I also believe that having a mechanism to reuse container configurations for those containers that dont't rely on @ServiceConnection would be nice in terms of avoiding code duplication and providing a good maintainability of the codebase.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

3 participants