单元测试运行原理探究

简介: 单元测试是软件开发过程中的重要一环,好的单测可以帮助我们更早的发现问题,为系统的稳定运行提供保障。单测还是很好的说明文档,我们往往看单测用例就能够了解到作者对类的设计意图。代码重构时也离不开单测,丰富的单测用例会使我们重构代码时信心满满。虽然单测如此重要,但是一直来都不是很清楚其运行原理,也不知道为什么要做这样或那样的配置,这样终究是不行的,于是准备花时间探究下单测原理,并在此记录。

前言

单元测试是软件开发过程中的重要一环,好的单测可以帮助我们更早的发现问题,为系统的稳定运行提供保障。单测还是很好的说明文档,我们往往看单测用例就能够了解到作者对类的设计意图。代码重构时也离不开单测,丰富的单测用例会使我们重构代码时信心满满。虽然单测如此重要,但是一直来都不是很清楚其运行原理,也不知道为什么要做这样或那样的配置,这样终究是不行的,于是准备花时间探究下单测原理,并在此记录。

当在IDEA中Run单元测试时发生了什么?

首先,来看一下当我们直接通过IDEA运行单例时,IDEA帮忙做了哪些事情:

  1. 将工程源码和测试源码进行编译,输出到了target目录
  2. 通过java命令运行com.intellij.rt.junit.JUnitStarter,参数中指定了junit的版本以及单测用例名称
java com.intellij.rt.junit.JUnitStarter -ideVersion5-junit4 fung.MyTest,test

这里着重追下JUnitStarter的代码,该类在IDEA提供的junit-rt.jar插件包中,具体目录:/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit-rt.jar。可以将这个包引入到我们自己的工程项目中,方便阅读源码:

JUnitStarter的main函数

publicstaticvoidmain(String[] args) {
List<String>argList=newArrayList(Arrays.asList(args));
ArrayList<String>listeners=newArrayList();
String[] name=newString[1];
StringagentName=processParameters(argList, listeners, name);
if (!"com.intellij.junit5.JUnit5IdeaTestRunner".equals(agentName) &&!canWorkWithJUnitVersion(System.err, agentName)) {
System.exit(-3);
    }
if (!checkVersion(args, System.err)) {
System.exit(-3);
    }
String[] array= (String[])argList.toArray(newString[0]);
intexitCode=prepareStreamsAndStart(array, agentName, listeners, name[0]);
System.exit(exitCode);
}

这里主要有两个核心方法

...
// 处理参数,主要用来确定使用哪个版本的junit框架,同时根据入参填充listenersStringagentName=processParameters(argList, listeners, name);
...
// 启动测试intexitCode=prepareStreamsAndStart(array, agentName, listeners, name[0]);
...

接下来看下prepareStreamsAndStart方法运行的时序图,这里以JUnit4为例:

当IDEA确认好要启动的框架版本后,会通过类的全限定名称反射创建IdeaTestRunner的实例。这里以JUnit4为例,IDEA会实例化com.intellij.junit4.JUnit4IdeaTestRunner类对象并调用其startRunnerWithArgs方法,在该方法中会通过buildRequest方法构建org.junit.runner.Request,通过getDescription方法获取org.junit.runner.Description,最后创建org.junit.runner.JUnitCore实例并调用其run方法。

简而言之就是,IDEA最终会借助Junit4框架的能力启动并运行单测用例,所以接下来有必要对Junit4框架的源码做些深入的探究。

Junit4源码探究

Junit是一个由Java语言编写的单元测试框架,已在业界被广泛运用,其作者是大名鼎鼎的Kent Beck和Erich Gamma,前者是《重构:改善既有代码的设计》和《测试驱动开发》的作者,后者则是《设计模式》的作者,Eclipse之父。Junit4发布于2006年,虽然是老古董了,但其中所蕴含的设计理念和思想却并不过时,有必要认真探究一番。

首先我们还是从一个简单的单测用例开始:

publicclassMyTest {
publicstaticvoidmain(String[] args) {
JUnitCorerunner=newJUnitCore();
Requestrequest=Request.aClass(MyTest.class);
Resultresult=runner.run(request.getRunner());
System.out.println(JSON.toJSONString(result));
    }
@Testpublicvoidtest1() {
System.out.println("test1");
    }
@Testpublicvoidtest2() {
System.out.println("test2");
    }
@Testpublicvoidtest3() {
System.out.println("test3");
    }
}

这里我们不再通过IDEA的插件启动单元测试,而是直接通过main函数,核心代码如下:

publicstaticvoidmain(String[] args) {
// 1. 创建JUnitCore的实例JUnitCorerunner=newJUnitCore();
// 2. 通过单测类的Class对象构建RequestRequestrequest=Request.aClass(MyTest.class);
// 3. 运行单元测试Resultresult=runner.run(request.getRunner());
// 4. 打印结果System.out.println(JSON.toJSONString(result));
}

着重看下runner.run(request.getRunner()),先看run函的代码:

可以看到最终运行哪种类型的测试流程取决于传入的runner实例,即不同的Runner决定了不同的运行流程,通过实现类的名字可以大概猜一猜,JUnit4ClassRunner应该是JUnit4基本的测试流程,MockitoJUnitRunner应该是引入了Mockito的能力,SpringJUnit4ClassRunner应该和Spring有些联系,可能会启动Spring容器。

现在,我们回过头来看看runner.run(request.getRunner())中request.getRunner()的代码:

publicRunnergetRunner() {
if (runner==null) {
synchronized (runnerLock) {
if (runner==null) {
runner=newAllDefaultPossibilitiesBuilder(canUseSuiteMethod).safeRunnerForClass(fTestClass);
      }
    }
  }
returnrunner;
}
publicRunnersafeRunnerForClass(Class<?>testClass) {
try {
returnrunnerForClass(testClass);
  } catch (Throwablee) {
returnnewErrorReportingRunner(testClass, e);
  }
}
publicRunnerrunnerForClass(Class<?>testClass) throwsThrowable {
List<RunnerBuilder>builders=Arrays.asList(
ignoredBuilder(),
annotatedBuilder(),
suiteMethodBuilder(),
junit3Builder(),
junit4Builder()
  );
for (RunnerBuildereach : builders) {
Runnerrunner=each.safeRunnerForClass(testClass);
if (runner!=null) {
returnrunner;
    }
  }
returnnull;
}

可以看到Runner是基于传入的测试类(testClass)的信息选择的,这里的规则如下:

  1. 如果解析失败了,则返回ErrorReportingRunner
  2. 如果测试类上有@Ignore注解,则返回IgnoredClassRunner
  3. 如果测试类上有@RunWith注解,则使用@RunWith的值实例化一个Runner返回
  4. 如果canUseSuiteMethod=true,则返回SuiteMethod,其继承自JUnit38ClassRunner,是比较早期的JUnit版本了
  5. 如果JUnit版本在4之前,则返回JUnit38ClassRunner
  6. 如果上面都不满足,则返回BlockJUnit4ClassRunner,其表示的是一个标准的JUnit4测试模型

我们先前举的那个简单的例子返回的就是BlockJUnit4ClassRunner,那么就以BlockJUnit4ClassRunner为例,看下它的run方法是怎么执行的吧。

首先会先走到其父类ParentRunner中的run方法

@Overridepublicvoidrun(finalRunNotifiernotifier) {
EachTestNotifiertestNotifier=newEachTestNotifier(notifier,
getDescription());
try {
Statementstatement=classBlock(notifier);
statement.evaluate();
  } catch (AssumptionViolatedExceptione) {
testNotifier.addFailedAssumption(e);
  } catch (StoppedByUserExceptione) {
throwe;
  } catch (Throwablee) {
testNotifier.addFailure(e);
  }
}

这里有必要展开说下Statement,官方的解释是:Represents one or more actions to be taken at runtime in the course of running a JUnit test suite.

Statement可以简单理解为对可执行方法的封装和抽象,如RunBefores就是一个Statement,它封装了所有标记了@BeforeClass注解的方法,在运行单例类的用例之前会执行这些方法,运行完后RunBefores还会通过next.evaluate()运行后续的Statement。这里列举一下常见的Statement:

  • RunBefores,会先运行befores里封装的方法(一般是标记了@BeforeClass或@Before),再运行next.evaluate()
  • RunAfters,会先运行next.evaluate(),再运行afters里封装的方法(一般是标记了@AfterClass或@After)
  • InvokeMethod,直接运行testMethod中封装的方法

由此可见,整个单测的运行过程,实际上就是一系列Statement的运行过程,以之前的MyTest为例,它的Statement的执行过程大致可以概况如下:

还剩一个最后问题,实际被测试方法是如何被运行的呢?答案是反射调用。核心代码如下:

@Overridepublicvoidevaluate() throwsThrowable {
testMethod.invokeExplosively(target);
}
publicObjectinvokeExplosively(finalObjecttarget, finalObject... params)
throwsThrowable {
returnnewReflectiveCallable() {
@OverrideprotectedObjectrunReflectiveCall() throwsThrowable {
returnmethod.invoke(target, params);
    }
  }.run();
}

至此一个标准Junit4的单测用例的执行过程就分析完了,那么像Spring这种需要起容器的单测又是如何运行的呢?接下来就来探究一下。

Spring单测的探究

我们还是以一个简单的例子开始吧

@RunWith(SpringRunner.class)
@ContextConfiguration(locations= { "/spring/spring-mybeans.xml" })
publicclassSpringRunnerTest {
@AutowiredprivateMyTestBeanmyTestBean;
@Testpublicvoidtest() {
myTestBean.test();
    }
}

这里先粗滤的概括下运行单测时发生了什么。首先,@RunWith注解了该测试类,所以Junit框架会先用SpringRunnerTest.class作为参数创建SpringRunner的实例,然后调用SpringRunner的run方法运行测试,该方法中会启动Spring容器,加载@ContextConfiguration注解指定的Bean配置文件,同时也会处理@Autowired注解为SpringRunnerTest的实例注入myTestBean,最后运行test()测试用例。

简言之就是先通过SpringRunner启动Spring容器,然后运行测试方法。接下来探究一下SpringRunner启动Spring容器的过程。

publicfinalclassSpringRunnerextendsSpringJUnit4ClassRunner {
publicSpringRunner(Class<?>clazz) throwsInitializationError {
super(clazz);
  }
}
publicclassSpringJUnit4ClassRunnerextendsBlockJUnit4ClassRunner {
  ...
}

SpringRunner和SpringJUnit4ClassRunner实际是等价的,可以认为SpringRunner是SpringJUnit4ClassRunner的一个别名,这里着重看下SpringJUnit4ClassRunner类的实现。

SpringJUnit4ClassRunner继承了BlockJUnit4ClassRunner,前面着重分析过BlockJUnit4ClassRunner,它运行的是一个标准的JUnit4测试模型,SpringJUnit4ClassRunner则是在此基础上做了一些扩展,扩展的内容主要包括:

  1. 扩展了构造函数,多创建了一个TestContextManager实例。
  2. 扩展了createTest()方法,会额外调用TestContextManager的prepareTestInstance方法。
  3. 扩展了beforeClass,在执行@BeforeClass注解的方法前,会先调用TestContextManager的beforeTestClass方法。
  4. 扩展了before,在执行@Before注解的方法前,会先调用TestContextManager的beforeTestMethod方法。
  5. 扩展了afterClass,在执行@AfterClass注解的方法之后,会再调用TestContextManager的afterTestClass方法。
  6. 扩展了after,在执行@After注解的方法之后,会再调用TestContextManager的after方法。

TestContextManager是Spring测试框架的核心类,官方的解释是:TestContextManager is the main entry point into the Spring TestContext Framework. Specifically, a TestContextManager is responsible for managing a single TestContext.

TestContextManager管理着TestContext,而TestContext则是对ApplicationContext的一个再封装,可以把TestContext理解为增加了测试相关功能的Spring容器。 TestContextManager同时也管理着TestExecutionListeners,这里使用观察者模式提供了对测试运行过程中的关键节点(如beforeClass, afterClass等)的监听能力。

所以通过研究TestContextManager,TestContext和TestExecutionListeners的相关实现类的代码,就不难发现测试时Spring容器的启动秘密了。关键代码如下:

publicclassDefaultTestContextimplementsTestContext {
  ...
publicApplicationContextgetApplicationContext() {
ApplicationContextcontext=this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration);
if (contextinstanceofConfigurableApplicationContext) {
@SuppressWarnings("resource")
ConfigurableApplicationContextcac= (ConfigurableApplicationContext) context;
Assert.state(cac.isActive(), () ->"The ApplicationContext loaded for ["+this.mergedContextConfiguration+"] is not active. This may be due to one of the following reasons: "+"1) the context was closed programmatically by user code; "+"2) the context was closed during parallel test execution either "+"according to @DirtiesContext semantics or due to automatic eviction "+"from the ContextCache due to a maximum cache size policy.");
    }
returncontext;
  }
  ...
}

在DefaultTestContext的getApplicationContext方法中,调用了cacheAwareContextLoaderDelegate的loadContext,最终辗转调到Context的refresh方法,从而构筑起Spring容器上下文。时序图如下:

那么getApplicationContext方法又是在哪里被调用的呢?

前面介绍过,TestContextManager扩展了createTest()方法,会额外调用其prepareTestInstance方法。

publicvoidprepareTestInstance(ObjecttestInstance) throwsException {
if (logger.isTraceEnabled()) {
logger.trace("prepareTestInstance(): instance ["+testInstance+"]");
  }
getTestContext().updateState(testInstance, null, null);
for (TestExecutionListenertestExecutionListener : getTestExecutionListeners()) {
try {
testExecutionListener.prepareTestInstance(getTestContext());
    }
catch (Throwableex) {
if (logger.isErrorEnabled()) {
logger.error("Caught exception while allowing TestExecutionListener ["+testExecutionListener+"] to prepare test instance ["+testInstance+"]", ex);
      }
ReflectionUtils.rethrowException(ex);
    }
  }
}

prepareTestInstance方法中会调用所有TestExecutionListener的prepareTestInstance方法,其中有一个叫做DependencyInjectionTestExecutionListener的监听器会调到TestContext的getApplicationContext方法。

publicvoidprepareTestInstance(TestContexttestContext) throwsException {
if (logger.isDebugEnabled()) {
logger.debug("Performing dependency injection for test context ["+testContext+"].");
  }
injectDependencies(testContext);
}
protectedvoidinjectDependencies(TestContexttestContext) throwsException {
Objectbean=testContext.getTestInstance();
Class<?>clazz=testContext.getTestClass();
// 这里调用TestContext的getApplicationContext方法,构建Spring容器AutowireCapableBeanFactorybeanFactory=testContext.getApplicationContext().getAutowireCapableBeanFactory();
beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
beanFactory.initializeBean(bean, clazz.getName() +AutowireCapableBeanFactory.ORIGINAL_INSTANCE_SUFFIX);
testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
}

还剩最后一个问题,DependencyInjectionTestExecutionListener是如何被添加的呢?答案是spring.factories

至此Spring单测的启动过程就探究明白了,接下来看下SpringBoot的。

SpringBoot单测的探究

一个简单的SpringBoot单测例子

@RunWith(SpringRunner.class)
@SpringBootTest(classes=Application.class)
publicclassMySpringBootTest {
@AutowiredprivateMyTestBeanmyTestBean;
@Testpublicvoidtest() {
myTestBean.test();
    }
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented@Inherited@BootstrapWith(SpringBootTestContextBootstrapper.class)
public@interfaceSpringBootTest {
  ...
}

粗滤说明一下,这里还是通过SpringRunner的run方法启动测试,其中会启动Spring容器,而@SpringBootTest则提供了启动类,同时通过@BootstrapWith提供的SpringBootTestContextBootstrapper类丰富了TestContext的能力,使得其支持了SpringBoot的一些特性。这里着重探究下@BootstrapWith注解以及SpringBootTestContextBootstrapper。

前面在介绍TestContextManager时,并没有讲到其构造函数以及TestContext的实例化过程,这里将其补上

publicTestContextManager(Class<?>testClass) {
this(BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.createBootstrapContext(testClass)));
}
publicTestContextManager(TestContextBootstrappertestContextBootstrapper) {
this.testContext=testContextBootstrapper.buildTestContext();
registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners());
}
publicabstractclassAbstractTestContextBootstrapperimplementsTestContextBootstrapper {  
  ...
publicTestContextbuildTestContext() {
returnnewDefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),
getCacheAwareContextLoaderDelegate());
  }
  ...
}

构建DefaultTestContext需要传3个参数:

  • testClass,被测试的类元数据
  • MergedContextConfiguration,封装了声明在测试类上的与测试容器相关的注解,如@ContextConfiguration, @ActiveProfiles, @TestPropertySource
  • CacheAwareContextLoaderDelegate,用来loading或closing容器

那么当我们需要扩展TestContext的功能,或者不想用DefaultTestContext时,应该怎么办呢?最简单的方式自然是新写一个类实现TestContextBootstrapper接口,并覆写buildTestContext()方法,那么如何告诉测试框架要使用新的实现类呢?@BootstrapWith就派上用场了。这里来看下BootstrapUtils.resolveTestContextBootstrapper的代码

staticTestContextBootstrapperresolveTestContextBootstrapper(BootstrapContextbootstrapContext) {
Class<?>testClass=bootstrapContext.getTestClass();
Class<?>clazz=null;
try {
clazz=resolveExplicitTestContextBootstrapper(testClass);
if (clazz==null) {
clazz=resolveDefaultTestContextBootstrapper(testClass);
    }
if (logger.isDebugEnabled()) {
logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]",
testClass.getName(), clazz.getName()));
    }
TestContextBootstrappertestContextBootstrapper=BeanUtils.instantiateClass(clazz, TestContextBootstrapper.class);
testContextBootstrapper.setBootstrapContext(bootstrapContext);
returntestContextBootstrapper;
  }
  ...
}
privatestaticClass<?>resolveExplicitTestContextBootstrapper(Class<?>testClass) {
Set<BootstrapWith>annotations=AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
if (annotations.isEmpty()) {
returnnull;
  }
if (annotations.size() ==1) {
returnannotations.iterator().next().value();
  }
// 获取@BootstrapWith注解的值BootstrapWithbootstrapWith=testClass.getDeclaredAnnotation(BootstrapWith.class);
if (bootstrapWith!=null) {
returnbootstrapWith.value();
  }
thrownewIllegalStateException(String.format(
"Configuration error: found multiple declarations of @BootstrapWith for test class [%s]: %s",
testClass.getName(), annotations));
}

这里会通过@BootstrapWith注解的值,实例化定制的TestContextBootstrapper,从而提供定制的TestContext

SpringBootTestContextBootstrapper就是TestContextBootstrapper的实现类,它通过间接继承AbstractTestContextBootstrapper类扩展了创建TestContext的能力,这些扩展主要包括:

  1. 将ContextLoader替换为了SpringBootContextLoader
  2. 增加了DefaultTestExecutionListenersPostProcessor对TestExecutionListener进行增强处理
  3. 增加了对webApplicationType的处理

接下来看下SpringBootContextLoader的相关代码

publicclassSpringBootContextLoaderextendsAbstractContextLoader {
@OverridepublicApplicationContextloadContext(MergedContextConfigurationconfig)
throwsException {
Class<?>[] configClasses=config.getClasses();
String[] configLocations=config.getLocations();
Assert.state(
!ObjectUtils.isEmpty(configClasses)
||!ObjectUtils.isEmpty(configLocations),
        () ->"No configuration classes "+"or locations found in @SpringApplicationConfiguration. "+"For default configuration detection to work you need "+"Spring 4.0.3 or better (found "+SpringVersion.getVersion()
+").");
SpringApplicationapplication=getSpringApplication();
// 设置mainApplicationClassapplication.setMainApplicationClass(config.getTestClass());
// 设置primarySourcesapplication.addPrimarySources(Arrays.asList(configClasses));
// 添加configLocationsapplication.getSources().addAll(Arrays.asList(configLocations));
// 获取environmentConfigurableEnvironmentenvironment=getEnvironment();
if (!ObjectUtils.isEmpty(config.getActiveProfiles())) {
setActiveProfiles(environment, config.getActiveProfiles());
    }
ResourceLoaderresourceLoader= (application.getResourceLoader() !=null)
?application.getResourceLoader()
        : newDefaultResourceLoader(getClass().getClassLoader());
TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment,
resourceLoader, config.getPropertySourceLocations());
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment,
getInlinedProperties(config));
application.setEnvironment(environment);
// 获取并设置initializersList<ApplicationContextInitializer<?>>initializers=getInitializers(config,
application);
if (configinstanceofWebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.SERVLET);
if (!isEmbeddedWebEnvironment(config)) {
newWebConfigurer().configure(config, application, initializers);
      }
    }
elseif (configinstanceofReactiveWebMergedContextConfiguration) {
application.setWebApplicationType(WebApplicationType.REACTIVE);
if (!isEmbeddedWebEnvironment(config)) {
newReactiveWebConfigurer().configure(application);
      }
    }
else {
application.setWebApplicationType(WebApplicationType.NONE);
    }
application.setInitializers(initializers);
// 运行SpringBoot应用returnapplication.run();
  }
}

可以看到这里构建了SpringApplication,设置了mainApplicationClass,设置了primarySources,设置了initializers,最终通过application.run()启动了SpringBoot应用。

至此SpringBoot单测的启动过程也探究明白了,接下来看下Maven插件是如何运行单测的。

Maven插件如何运行单测

我们知道maven是通过一系列的插件帮助我们完成项目开发过程中的构建、测试、打包、部署等动作的,当在Console中运行maven clean test命令时,maven会依次运行以下goal:

  • maven-clean-plugin:2.5:clean,用于清理target目录
  • maven-resources-plugin:2.6:resources,将主工程目录下的资源文件移动到target目录下的classes目录中
  • maven-compiler-plugin:3.1:compile,将主工程目录下的java源码编译为字节码,并移动到target目录下的classes目录中
  • maven-resources-plugin:2.6:testResources,将测试工程目录下的资源文件移动到target目录下的test-classes目录中
  • maven-compiler-plugin:3.1:testCompile,将测试工程目录下的java源码编译为字节码,并移动到target目录下的classes目录中
  • maven-surefire-plugin:2.12.4:test,运行单测

我们扒下maven-surefire-plugin插件的代码看一下。首先引入下maven-surefire-plugin和surefire-junit4包,方便我们查看代码:

<dependency><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.9</version></dependency><dependency><groupId>org.apache.maven.surefire</groupId><artifactId>surefire-junit4</artifactId><version>3.0.0-M7</version></dependency>

核心代码在org.apache.maven.plugin.surefire.AbstractSurefireMojo#execute中,这里就不贴代码了,有兴趣的可以自己看下。总之这里会用JUnit4ProviderInfo中的信息通过反射实例化JUnit4Provider对象,然后调用其invoke方法,在改方法中会最终实例化Runner并调用其run方法。核心代码如下:

privatestaticvoidexecute( Class<?>testClass, Notifiernotifier, Filterfilter )
{
finalintclassModifiers=testClass.getModifiers();
if ( !isAbstract( classModifiers ) &&!isInterface( classModifiers ) )
  {
Requestrequest=aClass( testClass );
if ( filter!=null )
    {
request=request.filterWith( filter );
    }
Runnerrunner=request.getRunner();
if ( countTestsInRunner( runner.getDescription() ) !=0 )
    {
runner.run( notifier );
    }
  }
}

总结

至此单元测试运行的相关原理就探究完了,我们来回顾下有哪些内容吧

  1. 通过IDEA直接运行单测时,会通过JUnitStarter的main方法作为入口,最终调用Junit运行单元测试。
  2. Junit4将@Before、@Test、@After这些注解打标的方法都抽象成了Statement,整个单测的运行过程,实际上就是一系列Statement的运行过程。方法的调用是通过反射的方式实现的。
  3. 借助于@RunWith(SpringRunner.class)注解,测试框架会运行SpringRunner实例的run方法,通过TestContextManager创建TestContext,并启动Spring容器。SpringRunner和SpringJUnit4ClassRunner实际上是等价的。
  4. 借助于@SpringBootTest和@BootstrapWith(SpringBootTestContextBootstrapper.class)注解,测试框架通过SpringBootTestContextBootstrapper增强了TestContext,达到了启动SpringBoot应用的目的。
  5. Maven通过运行maven-surefire-plugin:2.12.4:test启动单元测试,其核心是通过JUnit4Provider调用了JUnit框架的代码。
相关文章
|
2月前
|
监控 Java 测试技术
精准化测试原理简介
该文探讨了软件测试中的精准化测试问题,通过找不同游戏引出测试覆盖的挑战。文章指出,全面的测试覆盖并不现实,自动化测试虽有帮助但并非银弹,且面临成本和覆盖率局限。接着,文章提出需要“最强大脑”来快速识别代码差异、影响范围及测试覆盖率。为此,它介绍了通过语法分析器和字节码来定位代码差异,利用ASM进行调用链分析,并借助Jacoco进行覆盖率统计。此外,文章强调了增量覆盖率统计和调用链在接口测试中的重要性,同时提醒高覆盖率不代表高质量,测试策略应结合业务逻辑和代码审查。
26 2
|
4月前
|
测试技术 Shell API
Playwright系列(3):运行测试用例
Playwright系列(3):运行测试用例
|
4月前
|
测试技术
软件测试/测试开发/全日制|Pytest如何灵活地运行用例
软件测试/测试开发/全日制|Pytest如何灵活地运行用例
34 0
|
5月前
|
Ubuntu Linux 定位技术
Trinitycore学习之在Linux环境上搭建服务器并测试运行
Trinitycore学习之在Linux环境上搭建服务器并测试运行
78 0
|
4月前
|
监控 数据可视化 Java
jvm性能调优实战 - 31从测试到上线_如何分析JVM运行状况及合理优化
jvm性能调优实战 - 31从测试到上线_如何分析JVM运行状况及合理优化
54 1
|
2天前
|
前端开发 测试技术
前端自动化测试中的快照测试原理
快照测试用于前端自动化测试,通过比较当前应用状态与预存预期快照来检测UI变化。流程包括设置测试环境、捕获屏幕快照、保存预期快照、比较快照及处理差异。当快照比较出现差异时,测试工程师审查判断是否为预期变化或错误,确保应用一致性。这种方法在重构、样式更改和跨浏览器测试时提供有效回归测试,减少手动验证工作。
|
9天前
|
数据采集 DataWorks 关系型数据库
DataWorks操作报错合集之在DataWorks运行任务时出现链接超时,但在测试连通性时显示正常连通是什么原因导致的
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
21 0
|
10天前
|
SQL DataWorks Java
DataWorks操作报错合集之在阿里云 DataWorks 中,代码在开发测试阶段能够成功运行,但在提交后失败并报错“不支持https”如何解决
DataWorks是阿里云提供的一站式大数据开发与治理平台,支持数据集成、数据开发、数据服务、数据质量管理、数据安全管理等全流程数据处理。在使用DataWorks过程中,可能会遇到各种操作报错。以下是一些常见的报错情况及其可能的原因和解决方法。
25 1
DataWorks操作报错合集之在阿里云 DataWorks 中,代码在开发测试阶段能够成功运行,但在提交后失败并报错“不支持https”如何解决
|
10天前
|
测试技术 Python
python运行集成测试
【4月更文挑战第22天】
9 1
|
11天前
|
XML 测试技术 持续交付
python运行集成测试
【4月更文挑战第21天】
21 2