Don't call close methods for reusable testcontainers

Refine `TestcontainersLifecycleApplicationContextInitializer` so that
the `close()` method is not called for reusable containers.

Closes gh-35210
This commit is contained in:
Phillip Webb 2023-04-30 06:07:05 -07:00
parent e7357ba805
commit c13041201d
4 changed files with 165 additions and 17 deletions

View File

@ -18,6 +18,7 @@ package org.springframework.boot.testcontainers.lifecycle;
import org.testcontainers.lifecycle.Startable;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
@ -33,7 +34,9 @@ public class TestcontainersLifecycleApplicationContextInitializer
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
applicationContext.getBeanFactory().addBeanPostProcessor(new TestcontainersLifecycleBeanPostProcessor());
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
applicationContext.addBeanFactoryPostProcessor(new TestcontainersLifecycleBeanFactoryPostProcessor());
beanFactory.addBeanPostProcessor(new TestcontainersLifecycleBeanPostProcessor(beanFactory));
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testcontainers.lifecycle;
import org.testcontainers.lifecycle.Startable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* {@link BeanFactoryPostProcessor} to prevent {@link AutoCloseable} destruction calls so
* that {@link TestcontainersLifecycleBeanFactoryPostProcessor} can be smarter about which
* containers to close.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @see TestcontainersLifecycleApplicationContextInitializer
*/
@Order(Ordered.LOWEST_PRECEDENCE)
class TestcontainersLifecycleBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
for (String beanName : beanFactory.getBeanNamesForType(Startable.class, false, false)) {
try {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
String destroyMethodName = beanDefinition.getDestroyMethodName();
if (destroyMethodName == null || AbstractBeanDefinition.INFER_METHOD.equals(destroyMethodName)) {
beanDefinition.setDestroyMethodName("");
}
}
catch (NoSuchBeanDefinitionException ex) {
}
}
}
}

View File

@ -16,10 +16,17 @@
package org.springframework.boot.testcontainers.lifecycle;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.lifecycle.Startable;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
/**
* {@link BeanPostProcessor} to manage the lifecycle of {@link Startable startable
@ -29,7 +36,14 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
* @author Stephane Nicoll
* @see TestcontainersLifecycleApplicationContextInitializer
*/
class TestcontainersLifecycleBeanPostProcessor implements BeanPostProcessor {
@Order(Ordered.LOWEST_PRECEDENCE)
class TestcontainersLifecycleBeanPostProcessor implements DestructionAwareBeanPostProcessor {
private final ConfigurableListableBeanFactory beanFactory;
TestcontainersLifecycleBeanPostProcessor(ConfigurableListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
@ -39,4 +53,31 @@ class TestcontainersLifecycleBeanPostProcessor implements BeanPostProcessor {
return bean;
}
@Override
public boolean requiresDestruction(Object bean) {
return bean instanceof Startable;
}
@Override
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
if (bean instanceof Startable startable && !isDestroyedByFramework(beanName) && !isReusedContainer(bean)) {
startable.close();
}
}
private boolean isDestroyedByFramework(String beanName) {
try {
BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition(beanName);
String destroyMethodName = beanDefinition.getDestroyMethodName();
return !"".equals(destroyMethodName);
}
catch (NoSuchBeanDefinitionException ex) {
return false;
}
}
private boolean isReusedContainer(Object bean) {
return (bean instanceof GenericContainer<?> container) && container.isShouldBeReused();
}
}

View File

@ -17,43 +17,90 @@
package org.springframework.boot.testcontainers.lifecycle;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.lifecycle.Startable;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
/**
* Tests for {@link TestcontainersLifecycleApplicationContextInitializer}.
* Tests for {@link TestcontainersLifecycleApplicationContextInitializer} and
* {@link TestcontainersLifecycleBeanPostProcessor} and
* {@link TestcontainersLifecycleBeanFactoryPostProcessor}.
*
* @author Stephane Nicoll
* @author Phillip Webb
*/
class TestcontainersLifecycleApplicationContextInitializerTests {
@Test
void whenStartableBeanInvokesStartOnRefresh() {
Startable container = mock(Startable.class);
try (AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext()) {
applicationContext.registerBean("container", Startable.class, () -> container);
new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext);
then(container).shouldHaveNoInteractions();
applicationContext.refresh();
then(container).should().start();
}
AnnotationConfigApplicationContext applicationContext = createApplicationContext(container);
then(container).shouldHaveNoInteractions();
applicationContext.refresh();
then(container).should().start();
applicationContext.close();
}
@Test
void whenStartableBeanInvokesDestroyOnShutdown() {
Startable mock = mock(Startable.class);
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.registerBean("container", Startable.class, () -> mock);
new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext);
void whenStartableBeanInvokesCloseOnShutdown() {
Startable container = mock(Startable.class);
AnnotationConfigApplicationContext applicationContext = createApplicationContext(container);
applicationContext.refresh();
then(mock).should(never()).close();
then(container).should(never()).close();
applicationContext.close();
then(mock).should().close();
then(container).should(times(1)).close();
}
@Test
void whenReusableContainerBeanInvokesStartButNotClose() {
GenericContainer<?> container = mock(GenericContainer.class);
given(container.isShouldBeReused()).willReturn(true);
AnnotationConfigApplicationContext applicationContext = createApplicationContext(container);
then(container).shouldHaveNoInteractions();
applicationContext.refresh();
then(container).should().start();
applicationContext.close();
then(container).should(never()).close();
}
@Test
void whenReusableContainerBeanFromConfigurationInvokesStartButNotClose() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext);
applicationContext.register(ReusableContainerConfiguration.class);
applicationContext.refresh();
GenericContainer<?> container = applicationContext.getBean(GenericContainer.class);
then(container).should().start();
applicationContext.close();
then(container).should(never()).close();
}
private AnnotationConfigApplicationContext createApplicationContext(Startable container) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
new TestcontainersLifecycleApplicationContextInitializer().initialize(applicationContext);
applicationContext.registerBean("container", Startable.class, () -> container);
return applicationContext;
}
@Configuration
static class ReusableContainerConfiguration {
@Bean
GenericContainer<?> container() {
GenericContainer<?> container = mock(GenericContainer.class);
given(container.isShouldBeReused()).willReturn(true);
return container;
}
}
}