单元测试运行原理探究(一)

发表于:2022-12-01 09:52

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:阿里云云栖号    来源:博客园

  简介: 单元测试软件开发过程中的重要一环,好的单测可以帮助我们更早的发现问题,为系统的稳定运行提供保障。单测还是很好的说明文档,我们往往看单测用例就能够了解到作者对类的设计意图。代码重构时也离不开单测,丰富的单测用例会使我们重构代码时信心满满。虽然单测如此重要,但是一直来都不是很清楚其运行原理,也不知道为什么要做这样或那样的配置,这样终究是不行的,于是准备花时间探究下单测原理,并在此记录
  前言
  单元测试是软件开发过程中的重要一环,好的单测可以帮助我们更早的发现问题,为系统的稳定运行提供保障。单测还是很好的说明文档,我们往往看单测用例就能够了解到作者对类的设计意图。代码重构时也离不开单测,丰富的单测用例会使我们重构代码时信心满满。虽然单测如此重要,但是一直来都不是很清楚其运行原理,也不知道为什么要做这样或那样的配置,这样终究是不行的,于是准备花时间探究下单测原理,并在此记录。
  当在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)的信息选择的,这里的规则如下:
  ·如果解析失败了,则返回ErrorReportingRunner
  · 如果测试类上有@Ignore注解,则返回IgnoredClassRunner
  · 如果测试类上有@RunWith注解,则使用@RunWith的值实例化一个Runner返回
  · 如果canUseSuiteMethod=true,则返回SuiteMethod,其继承自JUnit38ClassRunner,是比较早期的JUnit版本了
  · 如果JUnit版本在4之前,则返回JUnit38ClassRunner
  · 如果上面都不满足,则返回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这种需要起容器的单测又是如何运行的呢?接下来就来探究一下。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号