美文网首页
调试 maven-surefire-plugin

调试 maven-surefire-plugin

作者: 蓝笔头 | 来源:发表于2020-09-23 18:06 被阅读0次

下载源码

打开一个 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 项目。

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 父类 AbstractSurefireMojoexecute() 方法开始打上断点。

image.png
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 调试按钮,开始调试!!!

image.png

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

通过在 consoletxtxml 中报告测试结果。

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>

相关文章

网友评论

      本文标题:调试 maven-surefire-plugin

      本文链接:https://www.haomeiwen.com/subject/ssvnyktx.html