Junit5 源码分析系列(二)
上文我们介绍了junit5框架架构和对应的入口方法,主要由idea插件调用并传入LauncherDiscoveryRequest对象,根据经验,我们知道测试框架需要解析对应的测试用例文件,生成测试用例,组装参数,然后才能执行,收集结果等;我们详细看下框架是怎么解析并生成测试用例的。
我们先看下相关重要的类数据结构:
//代表这次测试计划的抽象,里面包含所有的测试用例
public class TestPlan {
//TestIdentifier ,代表一个测试用例或者容器的唯一标识
//解析的根节点集合
private final Set<TestIdentifier> roots = Collections.synchronizedSet(new LinkedHashSet<>(4));
//后代TestIdentifier集合,key为 父节点的unigueId
private final Map<String, Set<TestIdentifier>> children = new ConcurrentHashMap<>(32);
//所有的TestIdentifier集合,包括父子节点,key为父子节点的unigueId
private final Map<String, TestIdentifier> allIdentifiers = new ConcurrentHashMap<>(32);
private final boolean containsTests;
/**
* Construct a new {@code TestPlan} from the supplied collection of
* {@link TestDescriptor TestDescriptors}.
*
* <p>Each supplied {@code TestDescriptor} is expected to be a descriptor
* for a {@link org.junit.platform.engine.TestEngine TestEngine}.
*
* @param engineDescriptors the engine test descriptors from which the test
* plan should be created; never {@code null}
* @return a new test plan
*/
"1.0") (status = INTERNAL, since =
public static TestPlan from(Collection<TestDescriptor> engineDescriptors) {
Preconditions.notNull(engineDescriptors, "Cannot create TestPlan from a null collection of TestDescriptors");
TestPlan testPlan = new TestPlan(engineDescriptors.stream().anyMatch(TestDescriptor::containsTests));
//Visitor function,为了执行testPlan.add 方法,下一行调用
Visitor visitor = descriptor -> testPlan.add(TestIdentifier.from(descriptor));
engineDescriptors.forEach(engineDescriptor -> engineDescriptor.accept(visitor));
return testPlan;
}
/**
构造方法,是否包含测试用例
*/
"1.4") (status = INTERNAL, since =
protected TestPlan(boolean containsTests) {
this.containsTests = containsTests;
}
....
}
InternalTestPlan, TestPlan的子类,使用了委托模式,扩展了属性,具体如下:
class InternalTestPlan extends TestPlan {
private static final Logger logger = LoggerFactory.getLogger(InternalTestPlan.class);
private final AtomicBoolean warningEmitted = new AtomicBoolean(false);
//这里由个Root对象,代表所有发现的TestEngines和TestDescriptor
private final Root root;
private final TestPlan delegate;
static InternalTestPlan from(Root root) {
TestPlan delegate = TestPlan.from(root.getEngineDescriptors());
return new InternalTestPlan(root, delegate);
}
......
}
Root 类详细信息如下:
class Root {
//TestEngine和TestDescriptor的关联集合,存放每个engin和对应的根节点testDescriptor
private final Map<TestEngine, TestDescriptor> testEngineDescriptors = new LinkedHashMap<>(4);
//插件传递过来的参数,从LauncherDiscoveryRequest对象中获取
private final ConfigurationParameters configurationParameters;
......
}
我们可以看到Root对象中涉及到了TestEngine和 TestDescriptor, 由上节介绍框架model所知,junit抽象了一个执行引擎,即TestEngine接口,统一由testEngine发现和执行测试用例,部分结构信息如下:
public interface TestEngine {
......
/**
* Discover tests according to the supplied {@link EngineDiscoveryRequest}.
*
* <p>The supplied {@link UniqueId} must be used as the unique ID of the
* returned root {@link TestDescriptor}. In addition, the {@code UniqueId}
* must be used to create unique IDs for children of the root's descriptor
* by calling {@link UniqueId#append}.
*
* @param discoveryRequest the discovery request; never {@code null}
* @param uniqueId the unique ID to be used for this test engine's
* {@code TestDescriptor}; never {@code null}
* @return the root {@code TestDescriptor} of this engine, typically an
* instance of {@code EngineDescriptor}
* @see org.junit.platform.engine.support.descriptor.EngineDescriptor
*/
TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId);
/**
* Execute tests according to the supplied {@link ExecutionRequest}.
*
* <p>The {@code request} passed to this method contains the root
* {@link TestDescriptor} that was previously returned by {@link #discover},
* the {@link EngineExecutionListener} to be notified of test execution
* events, and {@link ConfigurationParameters} that may influence test execution.
*
* @param request the request to execute tests for; never {@code null}
*/
void execute(ExecutionRequest request);
......
}
我们查看对应实现类
HierarchicalTestEngine为抽象类,主要实现了execute(ExecutionRequest request) 方法,后面执行测试用例的时候会涉及;JupiterTestEngine,一个官方的实现类,Junit5的Jupiter 测试引擎,即默认的testEngine,其中主要方法实现了TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) 方法,返回JupiterEngineDescriptor对象
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
JupiterConfiguration configuration = new CachingJupiterConfiguration(
new DefaultJupiterConfiguration(discoveryRequest.getConfigurationParameters()));
JupiterEngineDescriptor engineDescriptor = new JupiterEngineDescriptor(uniqueId, configuration);
new DiscoverySelectorResolver().resolveSelectors(discoveryRequest, engineDescriptor);
return engineDescriptor;
}
TestDescriptor,顾名思义,测试用例描述符,一个重要的接口,测试用例本身的抽象,我们看下实现类
介绍了这么多,我们看下发现和解析测试用例的关键方法:
private Root discoverRoot(LauncherDiscoveryRequest discoveryRequest, String phase) {
//初始化一个Root对象
Root root = new Root(discoveryRequest.getConfigurationParameters());
//遍历测试引擎,由父节点 engine->class->method 扫描对应测试类和测试方法
//最后生成根节点
for (TestEngine testEngine : this.testEngines) {
......
//发现并生成和关联各层级TestDescriptor对象,最后返回根节点engine testDescriptor
Optional<TestDescriptor> engineRoot = discoverEngineRoot(testEngine, discoveryRequest);
engineRoot.ifPresent(rootDescriptor -> root.add(testEngine, rootDescriptor));
}
//处理idea插件传入的后置过滤器,剪枝,即去掉需要过滤的descriptor
root.applyPostDiscoveryFilters(discoveryRequest);
//再次剪枝,去掉不包含测试用例相关的descriptor
root.prune();
return root;
}
discoverEngineRoot(testEngine, discoveryRequest)方法会调用刚才说的JupiterTestEngine中的discover方法,可以看到其中直接new了一个Selector解析器去处理传输来的discoveryRequest
JupiterEngineDescriptor engineDescriptor = new JupiterEngineDescriptor(uniqueId, configuration);
new DiscoverySelectorResolver().resolveSelectors(discoveryRequest, engineDescriptor);
下节将详细介绍Selector解析器解析用例文件的流程。
To be continued...