Optimize the Execution Time of Spring Integration Tests

Spring TestContext Framework

https://docs.spring.io/spring-framework/docs/5.2.8.RELEASE/spring-framework-reference/testing.html#testcontext-framework

TestContext

TestContext encapsulates the context in which a test is run (agnostic of the actual testing framework in use) and provides context management and caching support for the test instance for which it is responsible. The TestContext also delegates to a SmartContextLoader to load an ApplicationContext if requested.

TestContext 封装了测试运行的上下文(与使用的实际测试框架无关),并为其负责的测试实例提供上下文管理和缓存支持。 TestContext 还委托 SmartContextLoader 在请求时加载 ApplicationContext。

TestContextManager

TestContextManager is the main entry point into the Spring TestContext Framework and is responsible for managing a single TestContext and signaling events to each registered TestExecutionListener at well-defined test execution points:

TestContextManager 是 Spring TestContext 框架的主要入口点,负责管理单个 TestContext 并在明确定义的测试执行点向每个注册的 TestExecutionListener 发送信号事件:

  • Prior to any “before class” or “before all” methods of a particular testing framework.

  • Test instance post-processing.

  • Prior to any “before” or “before each” methods of a particular testing framework.

  • Immediately before execution of the test method but after test setup.

  • Immediately after execution of the test method but before test tear down.

  • After any “after” or “after each” methods of a particular testing framework.

  • After any “after class” or “after all” methods of a particular testing framework.

TestExecutionListener

TestExecutionListener defines the API for reacting to test-execution events published by the TestContextManager with which the listener is registered. See TestExecutionListener Configuration.

TestExecutionListener 定义了用于对注册侦听器的 TestContextManager 发布的测试执行事件做出反应的 API。 请参阅测试执行侦听器配置。

Spring TestContext Framework

关键在于MergedContextConfiguration

MergedContextConfiguration 的影响因素

测试入口

SpringBootTest

SpringBootTestContextBootstrapper

AbstractTestContextBootstrapper

@Override
	public final MergedContextConfiguration buildMergedContextConfiguration() {
		Class<?> testClass = getBootstrapContext().getTestClass();
		CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate = getCacheAwareContextLoaderDelegate();

		if (TestContextAnnotationUtils.findAnnotationDescriptorForTypes(
				testClass, ContextConfiguration.class, ContextHierarchy.class) == null) {
			return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate);
		}

		if (TestContextAnnotationUtils.findAnnotationDescriptor(testClass, ContextHierarchy.class) != null) {
			Map<String, List<ContextConfigurationAttributes>> hierarchyMap =
					ContextLoaderUtils.buildContextHierarchyMap(testClass);
			MergedContextConfiguration parentConfig = null;
			MergedContextConfiguration mergedConfig = null;

			for (List<ContextConfigurationAttributes> list : hierarchyMap.values()) {
				List<ContextConfigurationAttributes> reversedList = new ArrayList<>(list);
				Collections.reverse(reversedList);

				// Don't use the supplied testClass; instead ensure that we are
				// building the MCC for the actual test class that declared the
				// configuration for the current level in the context hierarchy.
				Assert.notEmpty(reversedList, "ContextConfigurationAttributes list must not be empty");
				Class<?> declaringClass = reversedList.get(0).getDeclaringClass();

				mergedConfig = buildMergedContextConfiguration(
						declaringClass, reversedList, parentConfig, cacheAwareContextLoaderDelegate, true);
				parentConfig = mergedConfig;
			}

			// Return the last level in the context hierarchy
			Assert.state(mergedConfig != null, "No merged context configuration");
			return mergedConfig;
		}
		else {
			return buildMergedContextConfiguration(testClass,
					ContextLoaderUtils.resolveContextConfigurationAttributes(testClass),
					null, cacheAwareContextLoaderDelegate, true);
		}
	}

我们先来看一下使用的构造函数

MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass,
 StringUtils.toStringArray(locations), 
ClassUtils.toClassArray(classes), 
ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList),
 ActiveProfilesUtils.resolveActiveProfiles(testClass), 
mergedTestPropertySources.getLocations(), 
mergedTestPropertySources.getProperties(),
 contextCustomizers, contextLoader,
 cacheAwareContextLoaderDelegate, parentConfig);

在看一下MergedContextConfiguration的解释

可以看出以下的注解都会导致MergedContextConfiguration变化,从而使集成测试运行中重新启动上下文。

  1. @ContextConfiguration

  2. @ContextHierarchy

  3. @ActiveProfiles

  4. @TestPropertySource

这里我们需要关注一下contextCustomizers 

private Set<ContextCustomizer> getContextCustomizers(Class<?> testClass,
			List<ContextConfigurationAttributes> configAttributes) {

		List<ContextCustomizerFactory> factories = getContextCustomizerFactories();
		Set<ContextCustomizer> customizers = new LinkedHashSet<>(factories.size());
		for (ContextCustomizerFactory factory : factories) {
			ContextCustomizer customizer = factory.createContextCustomizer(testClass, configAttributes);
			if (customizer != null) {
				customizers.add(customizer);
			}
		}
		return customizers;
	}

它会获取全部ContextCustomizerFactory的实现,并调用createContextCustomizer来创建ContextCustomizer 的list

/**
 * Factory for creating {@link ContextCustomizer ContextCustomizers}.
 *
 * <p>Factories are invoked after {@link ContextLoader ContextLoaders} have
 * processed context configuration attributes but before the
 * {@link MergedContextConfiguration} is created.
 *
 * <p>By default, the Spring TestContext Framework will use the
 * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader}
 * mechanism for loading factories configured in all {@code META-INF/spring.factories}
 * files on the classpath.
 *
 * @author Phillip Webb
 * @author Sam Brannen
 * @since 4.3
 */
@FunctionalInterface
public interface ContextCustomizerFactory {

	/**
	 * Create a {@link ContextCustomizer} that should be used to customize a
	 * {@link org.springframework.context.ConfigurableApplicationContext ConfigurableApplicationContext}
	 * before it is refreshed.
	 * @param testClass the test class
	 * @param configAttributes the list of context configuration attributes for
	 * the test class, ordered <em>bottom-up</em> (i.e., as if we were traversing
	 * up the class hierarchy); never {@code null} or empty
	 * @return a {@link ContextCustomizer} or {@code null} if no customizer should
	 * be used
	 */
	@Nullable
	ContextCustomizer createContextCustomizer(Class<?> testClass, List<ContextConfigurationAttributes> configAttributes);

}

其中最重要的的实现就是MockitoContextCustomizerFactory,


class MockitoContextCustomizerFactory implements ContextCustomizerFactory {
    MockitoContextCustomizerFactory() {
    }

    public ContextCustomizer createContextCustomizer(Class<?> testClass, List<ContextConfigurationAttributes> configAttributes) {
        DefinitionsParser parser = new DefinitionsParser();
        this.parseDefinitions(testClass, parser);
        return new MockitoContextCustomizer(parser.getDefinitions());
    }

    private void parseDefinitions(Class<?> testClass, DefinitionsParser parser) {
        parser.parse(testClass);
        if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
            this.parseDefinitions(testClass.getEnclosingClass(), parser);
        }

    }
}

而在DefinitionsParser的parse方法中会解析并将测试类中的MockBean 和SpyBean添加到Set<Definition> 最终也会促成MergedContextConfiguration的变化

 void parse(Class<?> source) {
        this.parseElement(source, (Class)null);
        ReflectionUtils.doWithFields(source, (element) -> {
            this.parseElement(element, source);
        });
    }

    private void parseElement(AnnotatedElement element, Class<?> source) {
        MergedAnnotations annotations = MergedAnnotations.from(element, SearchStrategy.SUPERCLASS);
        annotations.stream(MockBean.class).map(MergedAnnotation::synthesize).forEach((annotation) -> {
            this.parseMockBeanAnnotation(annotation, element, source);
        });
        annotations.stream(SpyBean.class).map(MergedAnnotation::synthesize).forEach((annotation) -> {
            this.parseSpyBeanAnnotation(annotation, element, source);
        });
    }

所以使用@MockBean和@SpyBean 也会造成上下文的重启。

所以使用@MockBean和@SpyBean 也会造成上下文的重启。

所以使用@MockBean和@SpyBean 也会造成上下文的重启。

  1. @MockBean

  2. @SpyBean

方案:

尽可能时相同上下文的IT继承同一个父类,然后在这个共同的父类中定义好共同的上下文以及相关的mock。

父类

package com.test.platform.test;

import com.test.platform.test.adaptor.UserServiceAdaptor;
import com.test.platform.test.adaptor.OrderServiceAdaptor;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.kafka.test.context.EmbeddedKafka;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;


@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration(classes = {
    IntegrationTestUtil.class,
})
@AutoConfigureMockMvc
@EmbeddedKafka(partitions = 1)
public abstract class AbstractIT {


    @MockBean
    protected UserServiceAdaptor userServiceAdaptor;


   
    @MockBean
    protected OrderServiceAdaptor orderServiceAdaptor;


}

子类

package com.test.platform.test.;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

import com.test.platform.test.AbstractIT;

import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.util.DigestUtils;

public class OrderControllerIT extends AbstractIT {

    

    @Test
    public void testAssignOrderToUser() {

        String orderId = UUID.randomUUID();
        String userId = UUID.randomUUID();
        User user =TestingDataUtil.buildUser(userId) ;

        when(userServiceAdaptor.get(eq(userId))).thenReturn(User);
        IntegrationTestUtil.assignOrderToUser(orderId, userId);
        Order order = IntegrationTestUtil.getOrder(orderId);
        
        assertEquals(userId, order.getUsreId());
        
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值