From e587753b1d78f16620b6efa69cecc7069e2466ea Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:59:56 +0200 Subject: [PATCH] =?UTF-8?q?Introduce=20@=E2=81=A0EasyMockBean=20bean=20ove?= =?UTF-8?q?rride=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces example support for a custom @⁠EasyMockBean annotation that allows tests to use EasyMock as the mocking framework for bean overrides in a test's ApplicationContext. The point of this exercise is to ensure that it is possible for third parties to introduce bean override support for mocking frameworks other than Mockito, and that they can do so with the APIs currently in place. Closes gh-33562 --- framework-platform/framework-platform.gradle | 1 + spring-test/spring-test.gradle | 1 + .../test/context/aot/AotIntegrationTests.java | 1 + .../bean/override/easymock/EasyMockBean.java | 64 +++++++++++ .../EasyMockBeanIntegrationTests.java | 105 ++++++++++++++++++ .../EasyMockBeanOverrideMetadata.java | 80 +++++++++++++ .../EasyMockBeanOverrideProcessor.java | 41 +++++++ .../bean/override/easymock/EasyMockBeans.java | 40 +++++++ .../EasyMockResetTestExecutionListener.java | 61 ++++++++++ 9 files changed, 394 insertions(+) create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBean.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanIntegrationTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideMetadata.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeanOverrideProcessor.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockBeans.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/bean/override/easymock/EasyMockResetTestExecutionListener.java 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(); + } + +}