代码编织梦想

前言

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

当在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函数

public static void main(String[] args) {
    List<String> argList = new ArrayList(Arrays.asList(args));
    ArrayList<String> listeners = new ArrayList();
    String[] name = new String[1];
    String agentName = 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(new String[0]);
    int exitCode = prepareStreamsAndStart(array, agentName, listeners, name[0]);
    System.exit(exitCode);
}

这里主要有两个核心方法

...
// 处理参数,主要用来确定使用哪个版本的junit框架,同时根据入参填充listeners
String agentName = processParameters(argList, listeners, name);
...
// 启动测试
int exitCode = 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年,虽然是老古董了,但其中所蕴含的设计理念和思想却并不过时,有必要认真探究一番。

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

public class MyTest {
    public static void main(String[] args) {
        JUnitCore runner = new JUnitCore();
        Request request = Request.aClass(MyTest.class);
        Result result = runner.run(request.getRunner());
        System.out.println(JSON.toJSONString(result));
    }
    @Test
    public void test1() {
        System.out.println("test1");
    }
    @Test
    public void test2() {
        System.out.println("test2");
    }
    @Test
    public void test3() {
        System.out.println("test3");
    }
}

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

public static void main(String[] args) {
  // 1. 创建JUnitCore的实例
  JUnitCore runner = new JUnitCore();
  // 2. 通过单测类的Class对象构建Request
  Request request = Request.aClass(MyTest.class);
  // 3. 运行单元测试
  Result result = 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()的代码:

public Runner getRunner() {
  if (runner == null) {
    synchronized (runnerLock) {
      if (runner == null) {
        runner = new AllDefaultPossibilitiesBuilder(canUseSuiteMethod).safeRunnerForClass(fTestClass);
      }
    }
  }
  return runner;
}
public Runner safeRunnerForClass(Class<?> testClass) {
  try {
    return runnerForClass(testClass);
  } catch (Throwable e) {
    return new ErrorReportingRunner(testClass, e);
  }
}
public Runner runnerForClass(Class<?> testClass) throws Throwable {
  List<RunnerBuilder> builders = Arrays.asList(
    ignoredBuilder(),
    annotatedBuilder(),
    suiteMethodBuilder(),
    junit3Builder(),
    junit4Builder()
  );
  for (RunnerBuilder each : builders) {
    Runner runner = each.safeRunnerForClass(testClass);
    if (runner != null) {
      return runner;
    }
  }
  return null;
}

可以看到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方法

@Override
public void run(final RunNotifier notifier) {
  EachTestNotifier testNotifier = new EachTestNotifier(notifier,
                                                       getDescription());
  try {
    Statement statement = classBlock(notifier);
    statement.evaluate();
  } catch (AssumptionViolatedException e) {
    testNotifier.addFailedAssumption(e);
  } catch (StoppedByUserException e) {
    throw e;
  } catch (Throwable e) {
    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的执行过程大致可以概况如下:

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

@Override
public void evaluate() throws Throwable {
  testMethod.invokeExplosively(target);
}
public Object invokeExplosively(final Object target, final Object... params)
  throws Throwable {
  return new ReflectiveCallable() {
    @Override
    protected Object runReflectiveCall() throws Throwable {
      return method.invoke(target, params);
    }
  }.run();
}

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

Spring单测的探究

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

@RunWith(SpringRunner.class)
@ContextConfiguration(locations = { "/spring/spring-mybeans.xml" })
public class SpringRunnerTest {
    @Autowired
    private MyTestBean myTestBean;
    @Test
    public void test() {
        myTestBean.test();
    }
}

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

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

public final class SpringRunner extends SpringJUnit4ClassRunner {
  public SpringRunner(Class<?> clazz) throws InitializationError {
    super(clazz);
  }
}
public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner {
  ...
}

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容器的启动秘密了。关键代码如下:

public class DefaultTestContext implements TestContext {
  ...
  public ApplicationContext getApplicationContext() {
    ApplicationContext context = this.cacheAwareContextLoaderDelegate.loadContext(this.mergedContextConfiguration);
    if (context instanceof ConfigurableApplicationContext) {
      @SuppressWarnings("resource")
      ConfigurableApplicationContext cac = (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.");
    }
    return context;
  }
  ...
}

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

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

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

public void prepareTestInstance(Object testInstance) throws Exception {
  if (logger.isTraceEnabled()) {
    logger.trace("prepareTestInstance(): instance [" + testInstance + "]");
  }
  getTestContext().updateState(testInstance, null, null);
  for (TestExecutionListener testExecutionListener : getTestExecutionListeners()) {
    try {
      testExecutionListener.prepareTestInstance(getTestContext());
    }
    catch (Throwable ex) {
      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方法。

public void prepareTestInstance(TestContext testContext) throws Exception {
  if (logger.isDebugEnabled()) {
    logger.debug("Performing dependency injection for test context [" + testContext + "].");
  }
  injectDependencies(testContext);
}
protected void injectDependencies(TestContext testContext) throws Exception {
   Object bean = testContext.getTestInstance();
   Class<?> clazz = testContext.getTestClass();
   
   // 这里调用TestContext的getApplicationContext方法,构建Spring容器
   AutowireCapableBeanFactory beanFactory = 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)
public class MySpringBootTest {
    @Autowired
    private MyTestBean myTestBean;
    @Test
    public void test() {
        myTestBean.test();
    }
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(SpringBootTestContextBootstrapper.class)
public @interface SpringBootTest {
  ...
}

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

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

public TestContextManager(Class<?> testClass) {
 this(BootstrapUtils.resolveTestContextBootstrapper(BootstrapUtils.createBootstrapContext(testClass)));
}
public TestContextManager(TestContextBootstrapper testContextBootstrapper) {
 this.testContext = testContextBootstrapper.buildTestContext();
 registerTestExecutionListeners(testContextBootstrapper.getTestExecutionListeners());
}
public abstract class AbstractTestContextBootstrapper implements TestContextBootstrapper {  
  ...
  public TestContext buildTestContext() {
    return new DefaultTestContext(getBootstrapContext().getTestClass(), buildMergedContextConfiguration(),
        getCacheAwareContextLoaderDelegate());
  }
  ...
}

构建DefaultTestContext需要传3个参数:

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

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

static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext bootstrapContext) {
  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()));
    }
    TestContextBootstrapper testContextBootstrapper =
      BeanUtils.instantiateClass(clazz, TestContextBootstrapper.class);
    testContextBootstrapper.setBootstrapContext(bootstrapContext);
    return testContextBootstrapper;
  }
  ...
}
private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) {
  Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class);
  if (annotations.isEmpty()) {
    return null;
  }
  if (annotations.size() == 1) {
    return annotations.iterator().next().value();
  }
  // 获取@BootstrapWith注解的值
  BootstrapWith bootstrapWith = testClass.getDeclaredAnnotation(BootstrapWith.class);
  if (bootstrapWith != null) {
    return bootstrapWith.value();
  }
  throw new IllegalStateException(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的相关代码

public class SpringBootContextLoader extends AbstractContextLoader {
  @Override
  public ApplicationContext loadContext(MergedContextConfiguration config)
      throws Exception {
    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()
            + ").");
    SpringApplication application = getSpringApplication();
    // 设置mainApplicationClass
    application.setMainApplicationClass(config.getTestClass());
    // 设置primarySources
    application.addPrimarySources(Arrays.asList(configClasses));
    // 添加configLocations
    application.getSources().addAll(Arrays.asList(configLocations));
    // 获取environment
    ConfigurableEnvironment environment = getEnvironment();
    if (!ObjectUtils.isEmpty(config.getActiveProfiles())) {
      setActiveProfiles(environment, config.getActiveProfiles());
    }
    ResourceLoader resourceLoader = (application.getResourceLoader() != null)
        ? application.getResourceLoader()
        : new DefaultResourceLoader(getClass().getClassLoader());
    TestPropertySourceUtils.addPropertiesFilesToEnvironment(environment,
        resourceLoader, config.getPropertySourceLocations());
    TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment,
        getInlinedProperties(config));
    application.setEnvironment(environment);
    // 获取并设置initializers
    List<ApplicationContextInitializer<?>> initializers = getInitializers(config,
        application);
    if (config instanceof WebMergedContextConfiguration) {
      application.setWebApplicationType(WebApplicationType.SERVLET);
      if (!isEmbeddedWebEnvironment(config)) {
        new WebConfigurer().configure(config, application, initializers);
      }
    }
    else if (config instanceof ReactiveWebMergedContextConfiguration) {
      application.setWebApplicationType(WebApplicationType.REACTIVE);
      if (!isEmbeddedWebEnvironment(config)) {
        new ReactiveWebConfigurer().configure(application);
      }
    }
    else {
      application.setWebApplicationType(WebApplicationType.NONE);
    }
    application.setInitializers(initializers);
    // 运行SpringBoot应用
    return application.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方法。核心代码如下:

private static void execute( Class<?> testClass, Notifier notifier, Filter filter )
{
  final int classModifiers = testClass.getModifiers();
  if ( !isAbstract( classModifiers ) && !isInterface( classModifiers ) )
  {
    Request request = aClass( testClass );
    if ( filter != null )
    {
      request = request.filterWith( filter );
    }
    Runner runner = 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框架的代码。

原文链接

本文为阿里云原创内容,未经允许不得转载

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yunqiinsight/article/details/127213628

那些阿里的年轻人_阿里云云栖号的博客-爱代码爱编程

摘要: 今天是年轻人的节日 十九年前,杭州城西一间狭小简陋的民房里 有一群年轻人 他们衣着朴素、口袋里也没什么钱 但每个人的眼神是坚定的、热烈的 他们每天挂在嘴边的 是梦想要做一件改变世界的事儿 1999年,一群杭州的年轻人离开北京,准备回杭创业 1999年,阿里人在湖畔花园合影 之后大家的办公室,终... 今天是年轻人的节日 十九年前,杭州城西一间

“龙井”开箱评测 |alibaba dragonwell 新手上路指南_阿里云云栖号的博客-爱代码爱编程

阿里巴巴有着最丰富的 Java 应用场景,覆盖电商,金融,物流等众多领域,是世界上最大的 Java 用户之一。 2019 年 3 月 21 日,阿里巴巴在北京阿里云峰会上正式宣布开源了 Alibaba Dragonwell 8 产品,并建立了 Alibaba Dragonwell 社区来为全球 Java 用户,特别是中文社区的 Java 用户提供长期支

安全同学讲 maven 间接依赖场景的仲裁机制_阿里云云栖号的博客-爱代码爱编程

一 背景 为什么想写此文 去年的Log4j-core的安全问题,再次把供应链安全推向了高潮。在供应链安全的场景,蚂蚁集团在静态代码扫描平台-STC和资产威胁透视平台-哈勃这2款产品在联合合作下,优势互补,很好的解决了直接依赖和间接依赖的场景。 但是由于STC是基于事前,受限于扫描效率存在遗漏的风险面,而哈勃又是基于事后,存在修复时间上的风险。基于

idc:云效产品能力no.1,领跑中国devops市场_阿里云云栖号的博客-爱代码爱编程

图源:IDC IDC 报告指出,阿里云云效 DevOps 具有以下4点核心优势: 先进性理念:持续以先进理念BizDevOps打造产品和服务客户,通过单/多项目敏捷协作、持续部署和交付、分级质量守护、云原生DevOps、场景化度量和改进等解决方案进行产品落地,帮助企业快速实现业务价值。 全链路数字化:建设基于价值流程图(VSM)的产研数字指标体系

硬之城如何基于 sae 打造数智化电子工业互联网平台_阿里云云栖号的博客-爱代码爱编程

本文根据硬之城 CEO 李六七先生在峰会上的分享整理而成 全球数字化时代已经到来,数字经济正推动生产方式、生活方式和治理方式的深刻变化,成为重组全球要素资源,重塑经济结构,改变全球竞争格局的关键力量。 云是连接现实与虚拟孪生世界的技术平台,具有广阔的想象空间,作为数字经济的技术方向,云原生技术正在以前所未有的速度成为企业快速发展的关节一环。8月11

鱼传科技:函数计算,只要用上就会觉得香_阿里云云栖号的博客-爱代码爱编程

深圳鱼传科技有限公司是专注以精准营销和互联网生态产品运营为核心的综合互联网营销推广服务商。通过整合全网优质媒体资源,并结合智能数据模型和 AI 标签算法,向企业提供包括流量矩阵搭建运营、媒介流量采买、投放模型设计、产品营销策划、数据监控分析、效果运营等多层次服务。作为函数计算的资深用户,鱼传科技的 CTO 和技术负责人跟我们聊了鱼传科技的 Serverle

app 隐私合规“免费”自动化检测_阿里云云栖号的博客-爱代码爱编程_app合规检测

一、为什么要进行App隐私合规检测 2021年11月1日《个人信息保护法》正式生效;今年6月14日,国家互联网信息办公室公布《移动互联网应用程序信息服务管理规定》,这是针对App的最强监管新规,于8月1日起正式实施。新规要求应用程序提供者和应用程序分发平台应当履行信息内容管理主体责任,建立健全信息内容安全管理、信息内容生态治理、数据安全、个人信息保护、未

fluid 助力阿里云 serverless 容器极致提速_阿里云云栖号的博客-爱代码爱编程

背景 数据对于当今互联网业务的重要性不言而喻,它几乎渗透到了当今这个世界的每一个角落。但单有数据是不够的,真正让数据产生价值的,是针对各业务场景运行的对大量数据的密集分析与计算,如机器学习、大数据分析、OLAP 聚合分析等等。近些年,随着数据规模的增大,这些对于资源有着更高要求的数据密集应用自然地导向了以弹性资源著称的云服务。 在这种数据密集应用上云的

跨模态学习能力再升级,easynlp 电商文图检索效果刷新 sota_阿里云云栖号的博客-爱代码爱编程

导读 多模态内容(例如图像、文本、语音、视频等)在互联网上的爆炸性增长推动了各种跨模态模型的研究与发展,支持了多种跨模态内容理解任务。在这些跨模态模型中,CLIP(Contrastive Language-Image Pre-training)是一种经典的文图跨模态检索模型,它在大规模图文数据集上进行了对比学习预训练,具有很强的文图跨模态表征学习能力。在

easynlp 带你实现中英文机器阅读理解_阿里云云栖号的博客-爱代码爱编程

导读 机器阅读理解是自然语言处理(NLP),特别是自然语言理解(NLU)领域最重要的研究方向之一。自1977年首次被提出以来,机器阅读理解已有近50年的发展史,历经“人工规则”、“传统机器学习”、“深度学习”、“大规模预训练模型”等多个发展阶段。 机器阅读理解旨在帮助人类从大量文本中,快速聚焦相关信息,降低人工信息获取成本,增加信息检索有效性。作为人工

polardb-x 全局二级索引_阿里云云栖号的博客-爱代码爱编程

背景 索引是数据库的基础组件,早在1970年代,SystemR 就已经通过增加索引来支持多维度查询。单机数据库中,索引主要按照用途和使用的数据结构分为 BTree 索引、Hash 索引、全文索引、空间索引等。通常,每张表中包含一个主键索引(Primary Index),主键索引以外的索引,统称为二级索引(Secondary Index)。 采用存储计算

微服务全链路灰度新能力-爱代码爱编程

背景 微服务体系架构中,服务之间的依赖关系错综复杂,有时某个功能发版依赖多个服务同时升级上线。我们希望可以对这些服务的新版本同时进行小流量灰度验证,这就是微服务架构中特有的全链路灰度场景,通过构建从网关到整个后端服务的环境隔离来对多个不同版本的服务进行灰度验证。在发布过程中,我们只需部署服务的灰度版本,流量在调用链路上流转时,由流经的网关、各个中间件以及

李飞飞:云原生数据库是大势所趋-爱代码爱编程

云原生数据库不是弯道超车,是换道超车。 数据库、芯片、操作系统并列为全球信息技术三大件,也是企业技术系统必不可少的核心,互联网、金融、政务、电信、制造等主要行业都依赖于数据库技术和产品。在云数据库诞生之前,以Oracle为代表的传统数据库巨头统治着这一领域。 技术领域的创新可分为两种,渐进性创新和破坏性创新。在一个格局稳定的行业中,具备优势地位的老

基于 openyurt 和 edgex 的云边端协同新可能_edgex和thingsboard-爱代码爱编程

2022 EdgeX 中国挑战赛暨中关村国际前沿科技创新大赛 EdgeX 专题赛正式拉开帷幕。本次大赛分设两大赛道:医疗、教育、消费行业赛道和能源、工业、供应链赛道。大赛致力于构建一个物联网及边缘计算的学习和分享平台,基于 EdgeX Foundry、OpenYurt 等开源技术,针对不同赛道的多个应用场景,以共享技术投资解决行业技术问题。 为帮助参赛选

弹性并行查询深度剖析-爱代码爱编程

背景 并行查询(Parallel Query)是自PolarDB MySQL诞生伊始就致力于研发的企业级查询加速功能,这与PolarDB的产品定位密切相关,基于云原生的计算存储分离使底层数据量远突破单机容量的限制,而针对更海量数据的复杂分析、报表类业务也成为用户自然而然的需求,同时由于PolarDB是服务于在线业务(OLTP)的关系数据库系统,用户会

理论与实践:如何写好一个方法_理论、实践、方法-爱代码爱编程

个人认为一个好的方法主要表现在可读性、可维护性、可复用性上,本文通过设计原则和代码规范两章来讲解如何提高方法的可读性、可维护性、可复用性。这些设计原则和代码规范更多的是表现一种思想,不仅仅可以用在方法上,也可以用在类上、模块上。 下面通过具体的例子来讲解。 设计原则 单一原则 单一职责解释是一个模块只负责完成一个职责或者功能,主要是提升方法的可维

浅谈ddd中的聚合_ddd 下根据没有聚合根-爱代码爱编程

在我看来并不是MVC的基础上增加领域层,使用充血模型,解耦基础服务,我的代码就符合DDD了。 为什么要使用DDD? DDD分为战略部分跟战术部分,相信大家都认同DDD的核心在战略而非战术。而战略方面的核心我认为在业务建模,领域划分、统一语言等都在为业务建模服务。 为什么业务建模重要? 以前的开发流程有什么问题? 先说结论,开发人员交付的程序对

招行架构师徐佳航:金融云原生与开源标准的共同生长_招商银行 v 开源-爱代码爱编程

云原生的技术价值喻示着它就是未来,加入到一个具有可延续性生命力的开源社区,可以帮助我们更快地到达那里。 ——徐佳航,KubeVela Maintainer,来自招商银行基础设施研发中心云平台及运维平台开发团队。 来自招商银行基础设施研发中心的徐佳航是 KubeVela 开源社区的一位新晋 Maintainer,目前在云平台及

阿里云基于全新 rocketmq 5.0 内核的落地实践_rockermq稳定版-爱代码爱编程

前言 在上个月结束的 RocketMQ Summit 全球开发者峰会中,Apache RocketMQ 社区发布了新一代 RocketMQ 的能力全景图,为众多开发者阐述 RocketMQ 5.0 这一大版本的技术定位与发展方向。 在过去七年大规模云计算实践中,RocketMQ 不断自我演进,今天,RocketMQ 正式迈进 5.0 时代。 从社

oscar 2022 开源产业大会polardb-爱代码爱编程

9月16日,OSCAR 2022 开源产业大会在京召开,会议由中国信息通信研究院、中国通信标准化协会主办,中国通信标准化协会云计算标准和开源推进委员会承办。此次会议以“千行百业 可信开源”为主题,邀请上百位专家大咖和国内主流的开源社区及成员单位共商开源发展路径,共建开源产业生态,以推动开源技术在千行百业的融合发展。 作为全球数据库领导者,阿里云数据库坚定