Spring测试分组

Posted by CaiJiahe on March 31, 2018

0x01 问题

在对Spring程序进行集成测试的时候,有的时候我们希望能对其进行分组测试。比如,某些需要依赖redis server的单元测试,在执行集成测试的时候我们并不希望运行它,这就需要我们对测试用例进行分组。 正常的思路是Profile+命令行参数来对测试用例进行分组,但是在我尝试了下之后发现如下配置仍然在解析依赖。

@RunWith(SpringRunner.class)
@SpringBootTest
@Profile("dog")
class DogTests {

}

@RunWith(SpringRunner.class)
@SpringBootTest
@Profile("cat")
class CatTests {

}

mvn test -Dspring.profiles.active=cat,发现两个测试用例还是执行了,分组执行的目的没有达到。

0x02 解决方案

spring-test提供了@IfProfileValue这个注解来对测试用例进行分组。

@RunWith(SpringRunner.class)
@SpringBootTest
@IfProfileValue(name = "group-key", value = "dog")
class DogTests {

}

@RunWith(SpringRunner.class)
@SpringBootTest
@IfProfileValue(name = "group-key", value = "cat")
class CatTests {

}

这样在执行测试的时候只需要mvn test -Dgroup-key=cat就可以执行CatTests,忽略DogTests。

0x03 源码分析

// spring-test o.s.test.context.junit4
public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner {
	@Override
	public void run(RunNotifier notifier) {
		if (!ProfileValueUtils.isTestEnabledInThisEnvironment(getTestClass().getJavaClass())) {
			notifier.fireTestIgnored(getDescription());
			return;
		}
		super.run(notifier);
	}
}

// spring-test o.s.test.annotation
public abstract class ProfileValueUtils {

	public static boolean isTestEnabledInThisEnvironment(Class<?> testClass) {
		IfProfileValue ifProfileValue = AnnotatedElementUtils.findMergedAnnotation(testClass, IfProfileValue.class);
		return isTestEnabledInThisEnvironment(retrieveProfileValueSource(testClass), ifProfileValue);
	}
	
	public static ProfileValueSource retrieveProfileValueSource(Class<?> testClass) {
		Assert.notNull(testClass, "testClass must not be null");

		Class<ProfileValueSourceConfiguration> annotationType = ProfileValueSourceConfiguration.class;
		ProfileValueSourceConfiguration config = AnnotatedElementUtils.findMergedAnnotation(testClass, annotationType);
		Class<? extends ProfileValueSource> profileValueSourceType;
		if (config != null) {
			profileValueSourceType = config.value();
		}
		else {
			profileValueSourceType = (Class<? extends ProfileValueSource>) AnnotationUtils.getDefaultValue(annotationType);
			Assert.state(profileValueSourceType != null, "No default ProfileValueSource class");
		}

		ProfileValueSource profileValueSource;
		if (SystemProfileValueSource.class == profileValueSourceType) {
			profileValueSource = SystemProfileValueSource.getInstance();
		}
		else {
			try {
				profileValueSource = ReflectionUtils.accessibleConstructor(profileValueSourceType).newInstance();
			}
			catch (Exception ex) {
				profileValueSource = SystemProfileValueSource.getInstance();
			}
		}

		return profileValueSource;
	}
	
}

可以看到在SpringJUnit4ClassRunner执行的时候会去判断下这个测试类@IfProfileValue的value和系统参数是否匹配,如果不匹配就忽略执行。而Profile是走的Spring Boot Condition注解的那套逻辑,如果在ComponentScan的basePackage中没扫到的类是无法走Profile的,上述Profile无法生效问题的原因是因为两个测试类定义在test目录下,在scanCandidateComponents的时候没有扫描到这两个测试类导致的。

	// spring-context o.s.context.annotation
	private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
		Set<BeanDefinition> candidates = new LinkedHashSet<>();
		try {
			String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					resolveBasePackage(basePackage) + '/' + this.resourcePattern;
			Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
			boolean traceEnabled = logger.isTraceEnabled();
			boolean debugEnabled = logger.isDebugEnabled();
			for (Resource resource : resources) {
				if (traceEnabled) {
					logger.trace("Scanning " + resource);
				}
				if (resource.isReadable()) {
					try {
						MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
						if (isCandidateComponent(metadataReader)) {
							ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
							sbd.setResource(resource);
							sbd.setSource(resource);
							if (isCandidateComponent(sbd)) {
								if (debugEnabled) {
									logger.debug("Identified candidate component class: " + resource);
								}
								candidates.add(sbd);
							}
					}
					catch (Throwable ex) {
						throw new BeanDefinitionStoreException(
								"Failed to read candidate component class: " + resource, ex);
					}
				}
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
		}
		return candidates;
	}