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