Introduce @⁠EasyMockBean bean override example

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
This commit is contained in:
Sam Brannen 2024-09-18 13:59:56 +02:00
parent 6c2cba5d8a
commit e587753b1d
9 changed files with 394 additions and 0 deletions

View File

@ -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")

View File

@ -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")

View File

@ -164,6 +164,7 @@ class AotIntegrationTests extends AbstractAotTests {
@Test
void endToEndTestsForSelectedTestClasses() {
List<Class<?>> 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

View File

@ -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.
* <p>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.
* <p>Defaults to {@link MockType#STRICT}.
*/
MockType mockType() default MockType.STRICT;
}

View File

@ -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";
}
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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<Object> beans = new ArrayList<>();
void add(Object bean) {
this.beans.add(bean);
}
void resetAll() {
this.beans.forEach(EasyMock::reset);
}
}

View File

@ -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();
}
}