下载源码
打开一个 Git-Bash
窗口,执行下面的命令。
meikai@DESKTOP-9TL46L9 MINGW64 /f/tmp
$ git clone https://github.com/apache/maven-surefire
Cloning into 'maven-surefire'...
remote: Enumerating objects: 71470, done.
remote: Total 71470 (delta 0), reused 0 (delta 0), pack-reused 71470
Receiving objects: 100% (71470/71470), 12.35 MiB | 8.05 MiB/s, done.
Resolving deltas: 100% (24181/24181), done.
Checking out files: 100% (1905/1905), done.
meikai@DESKTOP-9TL46L9 MINGW64 /f/tmp
$ cd maven-surefire/
meikai@DESKTOP-9TL46L9 MINGW64 /f/tmp/maven-surefire (master)
$ git tag | grep M5
surefire-3.0.0-M5
surefire-3.0.0-M5_vote-1
meikai@DESKTOP-9TL46L9 MINGW64 /f/tmp/maven-surefire (master)
$ git checkout surefire-3.0.0-M5
HEAD is now at 1eb35fd1e [maven-release-plugin] prepare release surefire-3.0.0-M5_vote-1
meikai@DESKTOP-9TL46L9 MINGW64 /f/tmp/maven-surefire ((surefire-3.0.0-M5_vote-1))
$ git log -n 2
commit 1eb35fd1ec6e54545c080079b23aacbb66e4936d (HEAD, tag: surefire-3.0.0-M5_vote-1, tag: surefire-3.0.0-M5)
Author: tibordigana <tibordigana@apache.org>
Date: Wed Jun 10 20:16:36 2020 +0200
[maven-release-plugin] prepare release surefire-3.0.0-M5_vote-1
commit 02cca4dc3fba70a17b3a8fecc07c374a35e49d15
Author: tibordigana <tibordigana@apache.org>
Date: Tue Jun 9 22:16:10 2020 +0200
[GH] performance problem on Windows nodes
用 IDEA
打开 maven-surefire
项目。
调试环境搭建
新建一个新项目,使用 3.0.0-M5
版本的 maven-surefire-plugin
。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
</plugin>
并添加一些 JUnit
测试用例,如下所示:
import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
@Slf4j
public class TestUtils {
@Test
public void test() {
log.info("record");
Assert.assertTrue(Utils.test(1, 2));
Assert.assertFalse(Utils.test(10, 2));
}
@ParameterizedTest
@MethodSource("dataProvider")
void test(int a, int b, boolean expected) {
log.info("record");
Assert.assertEquals(expected, Utils.test(a, b));
}
static Stream<Arguments> dataProvider() {
return Stream.of(
Arguments.of(1, 2, true),
Arguments.of(1, 2, true),
Arguments.of(1, 2, true)
);
}
}
上面包含了 JUnit4
(通过 org.junit.Test
注解标识)和 JUnit5
(通过 org.junit.jupiter.params.ParameterizedTest
注解标识)的测试用例。
然后使用 mvnDebug
命令运行 test
生命周期阶段。
# -DforkCount=0:不 fork 进程来运行单元测试
F:\work\private\english>mvnDebug -DforkCount=0 clean test
Listening for transport dt_socket at address: 8000
在 maven-surefire
项目中创建一个远程调试配置。
在 SurefirePlugin
父类 AbstractSurefireMojo
的 execute()
方法开始打上断点。
package org.apache.maven.plugin.surefire;
/**
* Run tests using Surefire.
*/
@Mojo( name = "test", defaultPhase = LifecyclePhase.TEST, threadSafe = true,
requiresDependencyResolution = ResolutionScope.TEST )
public class SurefirePlugin extends AbstractSurefireMojo implements SurefireReportParameters
点击 IDEA
工具栏的 Debug
调试按钮,开始调试!!!
maven-surefire-plugin:3.0.0-M5:test
日志打印结果如下所示:(已经略去非关键信息)
[INFO] --- maven-surefire-plugin:3.0.0-M5:test (default-test) @ english ---
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.nocompany.mk.english.BaseApplicationTests
...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 13.28 s - in com.nocompany.mk.english.BaseApplicationTests
[INFO] Running com.nocompany.mk.english.util.TestUtils
...
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.063 s - in com.nocompany.mk.english.util.TestUtils
[INFO] Running com.nocompany.mk.english.util.TestWordUtils
...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.057 s - in com.nocompany.mk.english.util.TestWordUtils
[INFO] Running com.nocompany.mk.english.util.TestUtils
...
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.073 s - in com.nocompany.mk.english.util.TestUtils
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 6, Failures: 0, Errors: 0, Skipped: 0
在 maven-surefire
项目中
- 通过搜索
" T E S T S"
字符串,定位到了DefaultReporterFactory
类的runStarting()
方法。 - 通过搜索
"Results:"
字符串,定位到了DefaultReporterFactory
类的runCompleted()
方法。 - 通过搜索
"Running "
字符串,定位到了ConsoleReporter
类的testSetStarting()
方法。 - 通过搜索
"Tests run:"
字符串,定位到了TestSetStats
类。-
TestSetStats
类的getColoredTestSetSummary()
方法被ConsoleReporter
类的testSetCompleted()
方法调用。
-
源码分析
TestSetRunListener
类
通过在 console
、txt
和 xml
中报告测试结果。
package org.apache.maven.plugin.surefire.report;
/**
* Reports data for a single test set.
*
*/
public class TestSetRunListener
implements RunListener, ConsoleOutputReceiver, ConsoleLogger {
@Override
public void testSetStarting( TestSetReportEntry report )
{
detailsForThis.testSetStart();
// 1. 控制台打印测试开始(如:Running com.nocompany.mk.english.BaseApplicationTests)
consoleReporter.testSetStarting( report );
consoleOutputReceiver.testSetStarting( report );
}
@Override
public void testSetCompleted( TestSetReportEntry report )
{
final WrappedReportEntry wrap = wrapTestSet( report );
final List<String> testResults =
briefOrPlainFormat ? detailsForThis.getTestResults() : Collections.<String>emptyList();
// 2. 测试报告输出到 txt 文件中
fileReporter.testSetCompleted( wrap, detailsForThis, testResults );
// 3. 测试报告输出到 xml 文件中
simpleXMLReporter.testSetCompleted( wrap, detailsForThis );
statisticsReporter.testSetCompleted();
consoleReporter.testSetCompleted( wrap, detailsForThis, testResults );
}
}
AbstractSurefireMojo
类
package org.apache.maven.plugin.surefire;
/**
* Abstract base class for running tests using Surefire.
*
* @author Stephen Connolly
* @version $Id: SurefirePlugin.java 945065 2010-05-17 10:26:22Z stephenc $
*/
public abstract class AbstractSurefireMojo extends AbstractMojo
implements SurefireExecutionParameters {
@Override
public void execute()
throws MojoExecutionException, MojoFailureException
{
cli = commandLineOptions();
// Stuff that should have been final
setupStuff();
Platform platform = PLATFORM.withJdkExecAttributesForTests( getEffectiveJvm() );
if ( verifyParameters() && !hasExecutedBefore() )
{
// 1. 扫描需要执行的测试类,默认扫描(**/Test*.java, **/*Test.java, **/*Tests.java, **/*TestCase.java, !**/*$*)
// 可以通过 -Dtest 手动指定测试类模式(如 -Dtest="IT*,*Test*" )
DefaultScanResult scan = scanForTestClasses();
// 2. 选择测试平台,并执行单元测试
executeAfterPreconditionsChecked( scan, platform );
}
}
private void executeAfterPreconditionsChecked( @Nonnull DefaultScanResult scanResult, @Nonnull Platform platform )
throws MojoExecutionException, MojoFailureException
{
// 3. 选择合适的测试平台(对于当前测试这里会返回 JUnitPlatformProviderInfo)
List<ProviderInfo> providers = createProviders( testClasspath );
// 4. 通过某个 provider 执行 scanResult 中的测试用例
executeProvider( provider, scanResult, testClasspath, platform, wrapper );
}
@Nonnull
private RunResult executeProvider( @Nonnull ProviderInfo provider, @Nonnull DefaultScanResult scanResult,
@Nonnull TestClassPath testClasspathWrapper, @Nonnull Platform platform,
@Nonnull ResolvePathResultWrapper resolvedJavaModularityResult )
throws MojoExecutionException, MojoFailureException, SurefireExecutionException, SurefireBooterForkException,
TestSetFailedException
{
if ( isNotForking() ) { // 不需要 fork,即 -DforkCount=0
// 5. 在 surefire plugin 同一个 VM 里面运行单元测试用例 suties
InPluginVMSurefireStarter surefireStarter = createInprocessStarter( provider, classLoaderConfiguration,
runOrderParameters, scanResult, platform, testClasspathWrapper );
return surefireStarter.runSuitesInProcess( scanResult );
}
else {
// 6. fork 一个进程,然后在 fork 进程中运行单元测试用例
forkStarter = createForkStarter( provider, forkConfiguration, classLoaderConfiguration,
runOrderParameters, getConsoleLogger(), scanResult,
testClasspathWrapper, platform, resolvedJavaModularityResult );
return forkStarter.run( effectiveProperties, scanResult );
}
}
}
InPluginVMSurefireStarter
类
package org.apache.maven.plugin.surefire;
/**
* Starts the provider in the same VM as the surefire plugin.
* <br>
* This part of the booter is always guaranteed to be in the
* same vm as the tests will be run in.
*
*/
public class InPluginVMSurefireStarter
{
public RunResult runSuitesInProcess( @Nonnull DefaultScanResult scanResult )
throws SurefireExecutionException, TestSetFailedException
{
// The test classloader must be constructed first to avoid issues with commons-logging until we properly
// separate the TestNG classloader
// 7. 调用 JUnitPlatformProvider 的 invoke 方法,运行单元测试
return invokeProvider( null, testClassLoader, factory, providerConfig, false, startupConfig, true );
}
}
JUnitPlatformProvider
类
package org.apache.maven.surefire.junitplatform;
/**
* JUnit 5 Platform Provider.
*/
public class JUnitPlatformProvider extends AbstractProvider
{
@Override
public RunResult invoke( Object forkTestSet )
throws TestSetFailedException, ReporterException
{
try
{
RunListener runListener = reporterFactory.createReporter();
// 8. 在 startCapture 方法中重定向标准输出和标准错误输出
startCapture( ( ConsoleOutputReceiver ) runListener );
// 9. 调用所有的测试用例
invokeAllTests( scanClasspath(), runListener );
}
return runResult;
}
private void invokeAllTests( TestsToRun testsToRun, RunListener runListener )
{
RunListenerAdapter adapter = new RunListenerAdapter( runListener );
// 10. 执行测试用例
execute( testsToRun, adapter );
}
private void execute( TestsToRun testsToRun, RunListenerAdapter adapter )
{
// 11. 调用 DefaultLauncher 类的 execute() 方法执行测试用例
launcher.execute( builder.build(), adapter );
}
}
DefaultLauncher
类
需要使用 junit-platform-launcher:1.6.2
版本。详见参考!
package org.junit.platform.launcher.core;
/**
* Default implementation of the {@link Launcher} API.
*
* <p>External clients can obtain an instance by invoking
* {@link LauncherFactory#create()}.
*/
class DefaultLauncher implements Launcher {
@Override
public void execute(LauncherDiscoveryRequest discoveryRequest, TestExecutionListener... listeners) {
Preconditions.notNull(discoveryRequest, "LauncherDiscoveryRequest must not be null");
Preconditions.notNull(listeners, "TestExecutionListener array must not be null");
Preconditions.containsNoNullElements(listeners, "individual listeners must not be null");
execute(InternalTestPlan.from(discoverRoot(discoveryRequest, "execution")), listeners);
}
private void execute(InternalTestPlan internalTestPlan, TestExecutionListener[] listeners) {
// 12. 分别使用 org.junit.jupiter.engine.JupiterTestEngine 和 org.junit.vintage.engine.VintageTestEngine 测试引擎执行单元测试
for (TestEngine testEngine : root.getTestEngines()) {
TestDescriptor engineDescriptor = root.getTestDescriptorFor(testEngine);
// 13. 使用一个具体的测试引擎执行单元测试
// 需要测试的类存在于 engineDescriptor 对象的 children 字段中
execute(engineDescriptor, engineExecutionListener, configurationParameters, testEngine);
}
}
private void execute(TestDescriptor engineDescriptor, EngineExecutionListener listener,
ConfigurationParameters configurationParameters, TestEngine testEngine) {
// 14. 调用执行引擎的 execute 方法
testEngine.execute(new ExecutionRequest(engineDescriptor, delayingListener, configurationParameters));
}
}
HierarchicalTestEngine
类
package org.junit.platform.engine.support.hierarchical;
public abstract class HierarchicalTestEngine<C extends EngineExecutionContext> implements TestEngine {
@Override
public final void execute(ExecutionRequest request) {
try (HierarchicalTestExecutorService executorService = createExecutorService(request)) {
C executionContext = createExecutionContext(request);
ThrowableCollector.Factory throwableCollectorFactory = createThrowableCollectorFactory(request);
// 15. 调用 HierarchicalTestExecutor 的 execute 方法
new HierarchicalTestExecutor<>(request, executionContext, executorService,
throwableCollectorFactory).execute().get();
}
catch (Exception exception) {
throw new JUnitException("Error executing tests for engine " + getId(), exception);
}
}
}
class HierarchicalTestExecutor<C extends EngineExecutionContext> {
Future<Void> execute() {
TestDescriptor rootTestDescriptor = this.request.getRootTestDescriptor();
EngineExecutionListener executionListener = this.request.getEngineExecutionListener();
NodeExecutionAdvisor executionAdvisor = new NodeTreeWalker().walk(rootTestDescriptor);
NodeTestTaskContext taskContext = new NodeTestTaskContext(executionListener, this.executorService,
this.throwableCollectorFactory, executionAdvisor);
NodeTestTask<C> rootTestTask = new NodeTestTask<>(taskContext, rootTestDescriptor);
rootTestTask.setParentContext(this.rootContext);
// 16. 执行 NodeTestTask 的 execute 方法
return this.executorService.submit(rootTestTask);
}
}
NodeTestTask
类
package org.junit.platform.engine.support.hierarchical;
class NodeTestTask<C extends EngineExecutionContext> implements TestTask {
@Override
public void execute() {
try {
throwableCollector = taskContext.getThrowableCollectorFactory().create();
prepare();
if (throwableCollector.isEmpty()) {
checkWhetherSkipped();
}
if (throwableCollector.isEmpty() && !skipResult.isSkipped()) {
executeRecursively(); // 17. 循环执行每个测试类
}
if (context != null) {
cleanUp();
}
reportCompletion();
}
}
}
参考
junit-platform-launcher
依赖版本一致
maven-surefire
项目
修改项目根目录的 pom.xml
文件。
<!-- 修改前 -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 修改后 -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.6.2</version>
</dependency>
单元测试项目(这里是 english
)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- spring-boot-starter-test pom.xml 文件中 junit-jupite 的版本 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.6.2</version>
<scope>compile</scope>
</dependency>
<!-- junit-jupiter pom.xml 文件中 junit-bom 的版本 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>5.6.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<!-- junit-bom pom.xml 文件中 junit-platform-launcher 的版本 -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>1.6.2</version>
</dependency>
网友评论