Support merging custom TELs with default TELs

Prior to this commit, if a custom TestExecutionListener was registered
via @TestExecutionListeners the defaults would not be registered. Thus,
if a user wanted to declare a custom listener and use the default
listeners, the user was forced to manually declare all default
listeners in addition to any custom listeners. This unfortunately
required that the user know exactly which listeners were registered by
default. Moreover, the set of default listeners can change from release
to release, and with the support for automatic discovery of default
listeners introduced in SPR-11466 it is no longer even possible to know
what the set of default TestExecutionListeners is before runtime.

This commit addresses this issue by introducing a mechanism for merging
custom declared listeners with the defaults for the current
environment. Specifically, @TestExecutionListeners supports a new
MergeMode that is used to control whether or not explicitly declared
listeners are merged with the default listeners when
@TestExecutionListeners is declared on a class that does not inherit
listeners from a superclass.

Issue: SPR-8854
This commit is contained in:
Sam Brannen
2014-08-15 02:21:08 +02:00
parent b3add794d7
commit 66250b1f8e
3 changed files with 270 additions and 118 deletions

View File

@@ -24,9 +24,9 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* {@code TestExecutionListeners} defines class-level metadata for
* configuring which {@link TestExecutionListener TestExecutionListeners} should
* be registered with a {@link TestContextManager}.
* {@code TestExecutionListeners} defines class-level metadata for configuring
* which {@link TestExecutionListener TestExecutionListeners} should be
* registered with a {@link TestContextManager}.
*
* <p>Typically, {@code @TestExecutionListeners} will be used in conjunction with
* {@link ContextConfiguration @ContextConfiguration}.
@@ -47,7 +47,40 @@ import java.lang.annotation.Target;
public @interface TestExecutionListeners {
/**
* Alias for {@link #listeners() listeners}.
* Enumeration of <em>modes</em> that dictate whether or not explicitly
* declared listeners are merged with the default listeners when
* {@code @TestExecutionListeners} is declared on a class that does
* <strong>not</strong> inherit listeners from a superclass.
* @since 4.1
*/
static enum MergeMode {
/**
* Indicates that locally declared listeners should replace the default
* listeners.
*/
REPLACE_DEFAULTS,
/**
* Indicates that locally declared listeners should be merged with the
* default listeners.
* <p>The merging algorithm ensures that duplicates are removed from
* the list and that the resulting set of merged listeners is sorted
* according to the semantics of
* {@link org.springframework.core.annotation.AnnotationAwareOrderComparator
* AnnotationAwareOrderComparator}. If a listener implements
* {@link org.springframework.core.Ordered Ordered} or is annotated
* with {@link org.springframework.core.annotation.Order @Order} it can
* influence the position in which it is merged with the defaults; otherwise,
* locally declared listeners will simply be appended to the list of default
* listeners when merged.
*/
MERGE_WITH_DEFAULTS,
}
/**
* Alias for {@link #listeners}.
*
* <p>This attribute may <strong>not</strong> be used in conjunction with
* {@link #listeners}, but it may be used instead of {@link #listeners}.
@@ -56,7 +89,7 @@ public @interface TestExecutionListeners {
/**
* The {@link TestExecutionListener TestExecutionListeners} to register with
* a {@link TestContextManager}.
* the {@link TestContextManager}.
*
* <p>This attribute may <strong>not</strong> be used in conjunction with
* {@link #value}, but it may be used instead of {@link #value}.
@@ -65,14 +98,15 @@ public @interface TestExecutionListeners {
* @see org.springframework.test.context.support.DependencyInjectionTestExecutionListener
* @see org.springframework.test.context.support.DirtiesContextTestExecutionListener
* @see org.springframework.test.context.transaction.TransactionalTestExecutionListener
* @see org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener
*/
Class<? extends TestExecutionListener>[] listeners() default {};
/**
* Whether or not {@link #value() TestExecutionListeners} from superclasses
* Whether or not {@link #listeners TestExecutionListeners} from superclasses
* should be <em>inherited</em>.
* <p>
* The default value is {@code true}, which means that an annotated
*
* <p>The default value is {@code true}, which means that an annotated
* class will <em>inherit</em> the listeners defined by an annotated
* superclass. Specifically, the listeners for an annotated class will be
* appended to the list of listeners defined by an annotated superclass.
@@ -106,4 +140,19 @@ public @interface TestExecutionListeners {
*/
boolean inheritListeners() default true;
/**
* The <em>merge mode</em> to use when {@code @TestExecutionListeners} is
* declared on a class that does <strong>not</strong> inherit listeners
* from a superclass.
* <p>Can be set to {@link MergeMode#MERGE_WITH_DEFAULTS MERGE_WITH_DEFAULTS}
* to have locally declared listeners <em>merged</em> with the default
* listeners.
* <p>The mode is ignored if listeners are inherited from a superclass.
* <p>Defaults to {@link MergeMode#REPLACE_DEFAULTS REPLACE_DEFAULTS}
* for backwards compatibility.
* @see MergeMode
* @since 4.1
*/
MergeMode mergeMode() default MergeMode.REPLACE_DEFAULTS;
}

View File

@@ -19,6 +19,7 @@ package org.springframework.test.context.support;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -45,6 +46,7 @@ import org.springframework.test.context.SmartContextLoader;
import org.springframework.test.context.TestContextBootstrapper;
import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.TestExecutionListeners.MergeMode;
import org.springframework.test.util.MetaAnnotationUtils;
import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor;
import org.springframework.util.Assert;
@@ -137,14 +139,37 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot
listenerClasses = valueListenerClasses;
}
if (listenerClasses != null) {
classesList.addAll(0, Arrays.<Class<? extends TestExecutionListener>> asList(listenerClasses));
boolean inheritListeners = annAttrs.getBoolean("inheritListeners");
AnnotationDescriptor<TestExecutionListeners> superDescriptor = MetaAnnotationUtils.findAnnotationDescriptor(
descriptor.getRootDeclaringClass().getSuperclass(), annotationType);
// If there are no listeners to inherit, we might need to merge the
// locally declared listeners with the defaults.
if ((!inheritListeners || superDescriptor == null)
&& (annAttrs.getEnum("mergeMode") == MergeMode.MERGE_WITH_DEFAULTS)) {
if (logger.isDebugEnabled()) {
logger.debug(String.format(
"Merging default listeners with listeners configured via @TestExecutionListeners for class [%s].",
clazz.getName()));
}
usingDefaults = true;
classesList.addAll(getDefaultTestExecutionListenerClasses());
}
descriptor = (annAttrs.getBoolean("inheritListeners") ? MetaAnnotationUtils.findAnnotationDescriptor(
descriptor.getRootDeclaringClass().getSuperclass(), annotationType) : null);
classesList.addAll(0, Arrays.<Class<? extends TestExecutionListener>> asList(listenerClasses));
descriptor = (inheritListeners ? superDescriptor : null);
}
}
// Remove possible duplicates if we loaded default listeners.
if (usingDefaults) {
Set<Class<? extends TestExecutionListener>> classesSet = new HashSet<Class<? extends TestExecutionListener>>();
classesSet.addAll(classesList);
classesList.clear();
classesList.addAll(classesSet);
}
List<TestExecutionListener> listeners = instantiateListeners(classesList);
// Sort by Ordered/@Order if we loaded default listeners.