diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index b54f7eadeae..f53ef365031 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -115,6 +115,7 @@ dependencies { api("org.codehaus.jettison:jettison:1.5.4") api("org.crac:crac:1.4.0") api("org.dom4j:dom4j:2.1.4") + api("org.easymock:easymock:5.4.0") api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.7") api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4") api("org.eclipse:yasson:2.0.4") diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index cfe5e5913a6..f0bc5a7f635 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -77,6 +77,7 @@ dependencies { exclude group: "commons-logging", module: "commons-logging" } testImplementation("org.awaitility:awaitility") + testImplementation("org.easymock:easymock") testImplementation("org.hibernate:hibernate-core-jakarta") testImplementation("org.hibernate:hibernate-validator") testImplementation("org.hsqldb:hsqldb") diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java index 7c23d97ea3a..e4258da0fd5 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java @@ -164,6 +164,7 @@ class AotIntegrationTests extends AbstractAotTests { @Test void endToEndTestsForSelectedTestClasses() { List> testClasses = List.of( + org.springframework.test.context.bean.override.easymock.EasyMockBeanIntegrationTests.class, org.springframework.test.context.bean.override.mockito.MockitoBeanForByNameLookupIntegrationTests.class, org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTests.class, org.springframework.test.context.junit4.ParameterizedDependencyInjectionTests.class diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBean.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBean.java new file mode 100644 index 00000000000..08b5ae07778 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBean.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2024 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.test.context.bean.override.easymock; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.easymock.MockType; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.test.context.bean.override.BeanOverride; + +/** + * {@code @EasyMockBean} is a field-level annotation that can be used in a test + * class to signal that a bean should be replaced with an {@link org.easymock.EasyMock + * EasyMock} mock. + * + * @author Sam Brannen + * @since 6.2 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@BeanOverride(EasyMockBeanOverrideProcessor.class) +public @interface EasyMockBean { + + /** + * Alias for {@link #name}. + */ + @AliasFor("name") + String value() default ""; + + /** + * The name of the bean to mock. + *

Defaults to an empty string to denote that the name of the annotated + * field should be used as the bean name. + */ + @AliasFor("value") + String name() default ""; + + /** + * The {@link MockType} to use when creating the mock. + *

Defaults to {@link MockType#STRICT}. + */ + MockType mockType() default MockType.STRICT; + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanIntegrationTests.java new file mode 100644 index 00000000000..d7bf47ae5a1 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanIntegrationTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2024 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.test.context.bean.override.easymock; + +import org.easymock.EasyMockSupport; +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.bean.override.example.ExampleService; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.reset; +import static org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS; + +/** + * Integration tests for {@link EasyMockBean @EasyMockBean}. + * + * @author Sam Brannen + * @since 6.2 + */ +@SpringJUnitConfig +@TestExecutionListeners(listeners = EasyMockResetTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS) +@TestMethodOrder(OrderAnnotation.class) +public class EasyMockBeanIntegrationTests { + + @Autowired + ApplicationContext ctx; + + @EasyMockBean + ExampleService service; + + @Test + @Order(1) + void test1() { + assertThat(ctx.getBean("service", ExampleService.class)) + .satisfies(this::assertIsEasyMock) + .isSameAs(service); + + // Before mock setup + assertThat(service.greeting()).isNull(); + reset(service); + + // After mock setup + expect(service.greeting()).andReturn("mocked"); + replay(service); + assertThat(service.greeting()).isEqualTo("mocked"); + } + + @Test + @Order(2) + void test2() { + assertThat(ctx.getBean("service", ExampleService.class)) + .satisfies(this::assertIsEasyMock) + .isSameAs(service); + + // Before mock setup + assertThat(service.greeting()).isNull(); + reset(service); + + // After mock setup + expect(service.greeting()).andReturn("mocked"); + replay(service); + assertThat(service.greeting()).isEqualTo("mocked"); + } + + + private void assertIsEasyMock(Object obj) { + assertThat(EasyMockSupport.isAMock(obj)).as("is EasyMock mock").isTrue(); + } + + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + ExampleService service() { + return () -> "enigma"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideMetadata.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideMetadata.java new file mode 100644 index 00000000000..accbcb6aa8f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideMetadata.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2024 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.test.context.bean.override.easymock; + +import java.lang.reflect.Field; + +import org.easymock.EasyMock; +import org.easymock.MockType; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.SingletonBeanRegistry; +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.test.context.bean.override.OverrideMetadata; + +import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_OR_CREATE_DEFINITION; + +/** + * {@link OverrideMetadata} that provides support for {@link EasyMockBean @EasyMockBean}. + * + * @author Sam Brannen + * @since 6.2 + */ +class EasyMockBeanOverrideMetadata extends OverrideMetadata { + + private final MockType mockType; + + + EasyMockBeanOverrideMetadata(Field field, Class typeToOverride, @Nullable String beanName, + MockType mockType) { + + super(field, ResolvableType.forClass(typeToOverride), beanName, REPLACE_OR_CREATE_DEFINITION); + this.mockType = mockType; + } + + + @Override + protected Object createOverride(String beanName, @Nullable BeanDefinition existingBeanDefinition, + @Nullable Object existingBeanInstance) { + + Class typeToMock = getBeanType().getRawClass(); + return EasyMock.mock(beanName, this.mockType, typeToMock); + } + + @Override + protected void track(Object mock, SingletonBeanRegistry singletonBeanRegistry) { + getEasyMockBeans(singletonBeanRegistry).add(mock); + } + + private EasyMockBeans getEasyMockBeans(SingletonBeanRegistry singletonBeanRegistry) { + String className = EasyMockBeans.class.getName(); + EasyMockBeans easyMockBeans = null; + try { + easyMockBeans = (EasyMockBeans) singletonBeanRegistry.getSingleton(className); + } + catch (NoSuchBeanDefinitionException ignored) { + } + if (easyMockBeans == null) { + easyMockBeans = new EasyMockBeans(); + singletonBeanRegistry.registerSingleton(className, easyMockBeans); + } + return easyMockBeans; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideProcessor.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideProcessor.java new file mode 100644 index 00000000000..e6a7a09f408 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideProcessor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2024 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.test.context.bean.override.easymock; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +import org.springframework.test.context.bean.override.BeanOverrideProcessor; +import org.springframework.test.context.bean.override.OverrideMetadata; +import org.springframework.util.StringUtils; + +/** + * {@link BeanOverrideProcessor} that provides support for {@link EasyMockBean @EasyMockBean}. + * + * @author Sam Brannen + * @since 6.2 + */ +class EasyMockBeanOverrideProcessor implements BeanOverrideProcessor { + + @Override + public OverrideMetadata createMetadata(Annotation annotation, Class testClass, Field field) { + EasyMockBean easyMockBean = (EasyMockBean) annotation; + String beanName = (StringUtils.hasText(easyMockBean.name()) ? easyMockBean.name() : field.getName()); + return new EasyMockBeanOverrideMetadata(field, field.getType(), beanName, easyMockBean.mockType()); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeans.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeans.java new file mode 100644 index 00000000000..f40925b0fd4 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeans.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2024 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.test.context.bean.override.easymock; + +import java.util.ArrayList; +import java.util.List; + +import org.easymock.EasyMock; + +/** + * @author Sam Brannen + * @since 6.2 + */ +class EasyMockBeans { + + private final List beans = new ArrayList<>(); + + void add(Object bean) { + this.beans.add(bean); + } + + void resetAll() { + this.beans.forEach(EasyMock::reset); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockResetTestExecutionListener.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockResetTestExecutionListener.java new file mode 100644 index 00000000000..bb5d46bfbb5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockResetTestExecutionListener.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2024 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.test.context.bean.override.easymock; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.Ordered; +import org.springframework.test.context.TestContext; +import org.springframework.test.context.support.AbstractTestExecutionListener; + +/** + * {@code TestExecutionListener} that provides support for resetting mocks + * created via {@link EasyMockBean @EasyMockBean}. + * + * @author Sam Brannen + * @since 6.2 + */ +class EasyMockResetTestExecutionListener extends AbstractTestExecutionListener { + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE - 100; + } + + @Override + public void beforeTestMethod(TestContext testContext) throws Exception { + resetMocks(testContext.getApplicationContext()); + } + + @Override + public void afterTestMethod(TestContext testContext) throws Exception { + resetMocks(testContext.getApplicationContext()); + } + + private void resetMocks(ApplicationContext applicationContext) { + if (applicationContext instanceof ConfigurableApplicationContext configurableContext) { + resetMocks(configurableContext); + } + } + + private void resetMocks(ConfigurableApplicationContext applicationContext) { + ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); + beanFactory.getBean(EasyMockBeans.class).resetAll(); + } + +}