桂花流程引擎技术开发系列之测试篇—PluggableProcessEngineTest类分析

232 阅读7分钟

篇章引言

桂花流程引擎,是基于Camunda 7.20+扩展,旨在满足国内精细化的审批需求的国产流程引擎。桂花流程引擎为桂云网络公司旗下的标准化流程产品,取名“桂花”,其一是桂云网络公司主商标“桂云”的实物化产品品牌延伸,其二是桂花是中国传统文化名花,其淡雅的清香给人一种古典而唯美的体验感官。桂花流程引擎中“桂花”为桂云网络公司指定在商标尼斯分类第0901组软件类产品的商标,本文由桂云网络OSG独家贡献。

在桂云网络公司开发桂花流程引擎的过程中,单元测试是一个无法避免的基础知识,今天将Camunda的单元测试主要类PluggableProcessEngineTest源码分析一遍。

一、单元测试基础知识

Camunda使用的也是junit单元测试,因此,不得不说明junit单元测试的基础知识。junit单元测试分junit3、junit4和junit5。junit3单元测试是旧版本的单元测试,其需要继承TestCase类,并需要将测试函数前缀test才能标明为测试函数。TestCase有很多的接口函数,其重要有setUp和tearDown两个函数,setUp函数标明在单元测试函数运行前执行setUp函数,而tearDown函数标明在单元测试函数之后运行,这有点类似于Spring Boot框架AOP切面编程的@Around注解。到了junit4版本,就取消了junit3的做法,改为使用@Test注解来标注单元测试函数,并且使用@Before注解和@After注解取代了junit3版本的setUp和tearDown函数,测试函数也没了必须test开头的规定。具体举案例说明:

package com.osgit.bpmn.core;
 
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class MyTest {
    private final static Logger LOGGER = LoggerFactory.getLogger(MyTest.class);
 
    @Before
    public void init() {
        LOGGER.info("初始化工作");
    }
 
    @After
    public void clear() {
        LOGGER.info("清除工作");
    }
 
    @Test
    // 单元测试函数体
    public void fooBar() throws Exception {
        LOGGER.info("测试函数体...");
    }
}

运行结果如下

14:08:36.522 [main] INFO  com.osgit.bpmn.core.MyTest - 初始化工作
14:08:36.525 [main] INFO  com.osgit.bpmn.core.MyTest - 测试函数体...
14:08:36.525 [main] INFO  com.osgit.bpmn.core.MyTest - 清除工作

@Rule注解

@Rule注解即junit4单元测试的规则类,junit4内置了很多测试规则,如TestRule类、Timeout类、ExternalResource类等,在此仅说明基类TestRule类,其余具体作用在此不做详细说明。

TestRule是测试规则基础类,其只有一个apply函数,该函数返回一个Statement实例,其中Statement为单元测试抽象类,statement.evaluate()函数即业务单位测试函数执行,而Description为单元测试的描述信息类,包含业务单元测试的类名、函数名、注解等信息。在statement.evaluate前后可加上其他处理,这样得到的效果其实与以上是相同的。

package com.osgit.bpmn.core;
 
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class MyTest {
    private final static Logger LOGGER = LoggerFactory.getLogger(MyTest.class);
 
    // 测试规则,加上@Rule注解
    @Rule
    public TestRule testRule = new TestRule() {
        @Override
        public Statement apply(Statement statement, Description description) {
            return new Statement() {
                @Override
                public void evaluate() throws Throwable {
                    LOGGER.info("测试开始。。。");
                    statement.evaluate();
                    LOGGER.info("测试结束。。。");
                }
            };
        }
    };
 
    @Test
    public void fooBar() throws Exception {
        LOGGER.info("测试函数体...");
    }
}

执行结果,与Spring Boot环绕编程结果一样

15:56:32.030 [main] INFO  com.osgit.bpmn.core.MyTest - 测试开始。。。
15:56:32.033 [main] INFO  com.osgit.bpmn.core.MyTest - 测试函数体...
15:56:32.033 [main] INFO  com.osgit.bpmn.core.MyTest - 测试结束。。。

TestWatcher

TestWatcher注解为junit5提供的注解,用于单元测试的监听,TestWatcher有starting和finished等接口函数,其中starting即单元测试前执行,而finished在单元执行后执行。其效果跟@Before和@After是一样的。

二、PluggableProcessEngineTest源码分析

在有junit基础知识的情况下,我们开始分析Camunda的单元测试基类的工作原理。直接上源码

package com.osgit.bpmn.core.util;
 
import com.osgit.bpmn.core.SpringTestConfig;
import org.camunda.bpm.engine.*;
import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.camunda.bpm.engine.impl.interceptor.Command;
import org.camunda.bpm.engine.impl.persistence.entity.JobEntity;
import org.camunda.bpm.engine.impl.util.ClockUtil;
import org.camunda.bpm.engine.runtime.ActivityInstance;
import org.camunda.bpm.engine.runtime.Job;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.RuleChain;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
import java.util.ArrayList;
import java.util.List;
 
 
public class PluggableProcessEngineTest {
 
  protected ProvidedProcessEngineRule engineRule = new ProvidedProcessEngineRule();
  protected ProcessEngineTestRule testRule = new ProcessEngineTestRule(engineRule);
 
  @Rule
  public RuleChain ruleChain = RuleChain.outerRule(engineRule).around(testRule);
 
  protected ProcessEngine processEngine;
  protected ProcessEngineConfigurationImpl processEngineConfiguration;
  protected RepositoryService repositoryService;
  protected RuntimeService runtimeService;
  protected TaskService taskService;
  protected FormService formService;
  protected HistoryService historyService;
  protected IdentityService identityService;
  protected ManagementService managementService;
  protected AuthorizationService authorizationService;
  protected CaseService caseService;
  protected FilterService filterService;
  protected ExternalTaskService externalTaskService;
  protected DecisionService decisionService;
 
  public PluggableProcessEngineTest() {
  }
 
  @Before
  public void initializeServices() {
    processEngine = engineRule.getProcessEngine();
    processEngineConfiguration = engineRule.getProcessEngineConfiguration();
    repositoryService = processEngine.getRepositoryService();
    runtimeService = processEngine.getRuntimeService();
    taskService = processEngine.getTaskService();
    formService = processEngine.getFormService();
    historyService = processEngine.getHistoryService();
    identityService = processEngine.getIdentityService();
    managementService = processEngine.getManagementService();
    authorizationService = processEngine.getAuthorizationService();
    caseService = processEngine.getCaseService();
    filterService = processEngine.getFilterService();
    externalTaskService = processEngine.getExternalTaskService();
    decisionService = processEngine.getDecisionService();
 
    initSpringBoot();
  }
 
  public void initSpringBoot() {
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringTestConfig.class);
    SpringUtil springUtil = applicationContext.getBean(SpringUtil.class);
    springUtil.setApplicationContext(applicationContext);
  }
 
  public ProcessEngine getProcessEngine() {
    return processEngine;
  }
 
  public boolean areJobsAvailable() {
    List<Job> list = managementService.createJobQuery().list();
    for (Job job : list) {
      if (!job.isSuspended() && job.getRetries() > 0 && (job.getDuedate() == null || ClockUtil.getCurrentTime().after(job.getDuedate()))) {
        return true;
      }
    }
    return false;
  }
 
  protected List<ActivityInstance> getInstancesForActivityId(ActivityInstance activityInstance, String activityId) {
    List<ActivityInstance> result = new ArrayList<>();
    if(activityInstance.getActivityId().equals(activityId)) {
      result.add(activityInstance);
    }
    for (ActivityInstance childInstance : activityInstance.getChildActivityInstances()) {
      result.addAll(getInstancesForActivityId(childInstance, activityId));
    }
    return result;
  }
 
  protected void deleteHistoryCleanupJobs() {
    final List<Job> jobs = historyService.findHistoryCleanupJobs();
    for (final Job job: jobs) {
      processEngineConfiguration.getCommandExecutorTxRequired().execute((Command<Void>) commandContext -> {
        commandContext.getJobManager().deleteJob((JobEntity) job);
        return null;
      });
    }
  }
 
}

由PluggableProcessEngineTest源码分析可知,注解的@Before函数体,其步骤是,先从engineRule中获得一个ProcessEngine实例,然后获得流程引擎的配置实例,再全部初始化各类的Service,最后是初始化SpringBoot上下文。那么,这个engineRule是什么东西?

这时,切换到ProvidedProcessEngineRule的源码

package com.osgit.bpmn.core.util;
 
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.ProcessEngineConfiguration;
import org.camunda.bpm.engine.test.ProcessEngineRule;
 
import java.util.concurrent.Callable;
 
public class ProvidedProcessEngineRule extends ProcessEngineRule {
 
  protected static ProcessEngine cachedProcessEngine;
  
  protected Callable<ProcessEngine> processEngineProvider;
 
  public ProvidedProcessEngineRule() {
    super(getOrInitializeCachedProcessEngine(), true);
  }
 
  public ProvidedProcessEngineRule(final ProcessEngineBootstrapRule bootstrapRule) {
    this(() -> bootstrapRule.getProcessEngine());
  }
 
  public ProvidedProcessEngineRule(Callable<ProcessEngine> processEngineProvider) {
    super(true);
    this.processEngineProvider = processEngineProvider;
  }
 
  @Override
  protected void initializeProcessEngine() {
 
    if (processEngineProvider != null) {
      try {
        this.processEngine = processEngineProvider.call();
      } catch (Exception e) {
        throw new RuntimeException("Could not get process engine", e);
      }
    }
    else {
      super.initializeProcessEngine();
    }
  }
  
  protected static ProcessEngine getOrInitializeCachedProcessEngine() {
    if (cachedProcessEngine == null) {
      cachedProcessEngine = ProcessEngineConfiguration
          .createProcessEngineConfigurationFromResource("camunda.cfg.xml")
          .buildProcessEngine();
    }
    return cachedProcessEngine;
  }
 
}

发现这个ProvidedProcessEngineRule类并没有看出与junit相关的信息,继续看父ProcessEngineRule,这时候终于发现继承与TestWatcher类,与junit息息相关。

package org.camunda.bpm.engine.test;
 
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.camunda.bpm.engine.AuthorizationService;
import org.camunda.bpm.engine.CaseService;
import org.camunda.bpm.engine.DecisionService;
import org.camunda.bpm.engine.ExternalTaskService;
import org.camunda.bpm.engine.FilterService;
import org.camunda.bpm.engine.FormService;
import org.camunda.bpm.engine.HistoryService;
import org.camunda.bpm.engine.IdentityService;
import org.camunda.bpm.engine.ManagementService;
import org.camunda.bpm.engine.ProcessEngine;
import org.camunda.bpm.engine.ProcessEngineServices;
import org.camunda.bpm.engine.RepositoryService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.impl.ProcessEngineImpl;
import org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.camunda.bpm.engine.impl.telemetry.PlatformTelemetryRegistry;
import org.camunda.bpm.engine.impl.test.RequiredDatabase;
import org.camunda.bpm.engine.impl.test.TestHelper;
import org.camunda.bpm.engine.impl.util.ClockUtil;
import org.junit.Assume;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;
 
 
public class ProcessEngineRule extends TestWatcher implements ProcessEngineServices {
 
  protected String configurationResource = "camunda.cfg.xml";
  protected String configurationResourceCompat = "activiti.cfg.xml";
  protected String deploymentId = null;
  protected List<String> additionalDeployments = new ArrayList<>();
 
  protected boolean ensureCleanAfterTest = false;
 
  protected ProcessEngine processEngine;
  protected ProcessEngineConfigurationImpl processEngineConfiguration;
  protected RepositoryService repositoryService;
  protected RuntimeService runtimeService;
  protected TaskService taskService;
  protected HistoryService historyService;
  protected IdentityService identityService;
  protected ManagementService managementService;
  protected FormService formService;
  protected FilterService filterService;
  protected AuthorizationService authorizationService;
  protected CaseService caseService;
  protected ExternalTaskService externalTaskService;
  protected DecisionService decisionService;
 
  public ProcessEngineRule() {
    this(false);
  }
 
  public ProcessEngineRule(boolean ensureCleanAfterTest) {
    this.ensureCleanAfterTest = ensureCleanAfterTest;
  }
 
  public ProcessEngineRule(String configurationResource) {
    this(configurationResource, false);
  }
 
  public ProcessEngineRule(String configurationResource, boolean ensureCleanAfterTest) {
    this.configurationResource = configurationResource;
    this.ensureCleanAfterTest = ensureCleanAfterTest;
  }
 
  public ProcessEngineRule(ProcessEngine processEngine) {
    this(processEngine, false);
  }
 
  public ProcessEngineRule(ProcessEngine processEngine, boolean ensureCleanAfterTest) {
    this.processEngine = processEngine;
    this.ensureCleanAfterTest = ensureCleanAfterTest;
  }
 
  @Override
  public void starting(Description description) {
    String methodName = description.getMethodName();
    if (methodName != null) {
      // cut off method variant suffix "[variant name]" for parameterized tests
      int methodNameVariantStart = description.getMethodName().indexOf('[');
      int methodNameEnd = methodNameVariantStart < 0 ? description.getMethodName().length() : methodNameVariantStart;
      methodName = description.getMethodName().substring(0, methodNameEnd);
    }
    deploymentId = TestHelper.annotationDeploymentSetUp(processEngine, description.getTestClass(), methodName,
        description.getAnnotation(Deployment.class));
  }
 
  @Override
  public Statement apply(final Statement base, final Description description) {
 
    if (processEngine == null) {
      initializeProcessEngine();
    }
 
    initializeServices();
 
    Class<?> testClass = description.getTestClass();
    String methodName = description.getMethodName();
 
    RequiredHistoryLevel reqHistoryLevel = description.getAnnotation(RequiredHistoryLevel.class);
    boolean hasRequiredHistoryLevel = TestHelper.annotationRequiredHistoryLevelCheck(processEngine,
        reqHistoryLevel, testClass, methodName);
 
    RequiredDatabase requiredDatabase = description.getAnnotation(RequiredDatabase.class);
    boolean runsWithRequiredDatabase = TestHelper.annotationRequiredDatabaseCheck(processEngine,
        requiredDatabase, testClass, methodName);
    return new Statement() {
 
      @Override
      public void evaluate() throws Throwable {
        Assume.assumeTrue("ignored because the current history level is too low", hasRequiredHistoryLevel);
        Assume.assumeTrue("ignored because the database doesn't match the required ones", runsWithRequiredDatabase);
        ProcessEngineRule.super.apply(base, description).evaluate();
      }
    };
  }
 
  protected void initializeProcessEngine() {
    try {
      processEngine = TestHelper.getProcessEngine(configurationResource);
    } catch (RuntimeException ex) {
      if (ex.getCause() != null && ex.getCause() instanceof FileNotFoundException) {
        processEngine = TestHelper.getProcessEngine(configurationResourceCompat);
      } else {
        throw ex;
      }
    }
  }
 
  protected void initializeServices() {
    processEngineConfiguration = ((ProcessEngineImpl) processEngine).getProcessEngineConfiguration();
    repositoryService = processEngine.getRepositoryService();
    runtimeService = processEngine.getRuntimeService();
    taskService = processEngine.getTaskService();
    historyService = processEngine.getHistoryService();
    identityService = processEngine.getIdentityService();
    managementService = processEngine.getManagementService();
    formService = processEngine.getFormService();
    authorizationService = processEngine.getAuthorizationService();
    caseService = processEngine.getCaseService();
    filterService = processEngine.getFilterService();
    externalTaskService = processEngine.getExternalTaskService();
    decisionService = processEngine.getDecisionService();
  }
 
  protected void clearServiceReferences() {
    processEngineConfiguration = null;
    repositoryService = null;
    runtimeService = null;
    taskService = null;
    formService = null;
    historyService = null;
    identityService = null;
    managementService = null;
    authorizationService = null;
    caseService = null;
    filterService = null;
    externalTaskService = null;
    decisionService = null;
  }
 
  @Override
  public void finished(Description description) {
    identityService.clearAuthentication();
    processEngine.getProcessEngineConfiguration().setTenantCheckEnabled(true);
 
    TestHelper.annotationDeploymentTearDown(processEngine, deploymentId, description.getTestClass(), description.getMethodName());
    for (String additionalDeployment : additionalDeployments) {
      TestHelper.deleteDeployment(processEngine, additionalDeployment);
    }
 
    TestHelper.deleteCommonRemarkAll(processEngine);
 
    if (ensureCleanAfterTest) {
      TestHelper.assertAndEnsureCleanDbAndCache(processEngine);
    }
 
    TestHelper.resetIdGenerator(processEngineConfiguration);
    ClockUtil.reset();
 
 
    clearServiceReferences();
 
    PlatformTelemetryRegistry.clear();
  }
 
  public void setCurrentTime(Date currentTime) {
    ClockUtil.setCurrentTime(currentTime);
  }
 
  public String getConfigurationResource() {
    return configurationResource;
  }
 
  public void setConfigurationResource(String configurationResource) {
    this.configurationResource = configurationResource;
  }
 
  public ProcessEngine getProcessEngine() {
    return processEngine;
  }
 
  public void setProcessEngine(ProcessEngine processEngine) {
    this.processEngine = processEngine;
  }
 
  public ProcessEngineConfigurationImpl getProcessEngineConfiguration() {
    return processEngineConfiguration;
  }
 
  public void setProcessEngineConfiguration(ProcessEngineConfigurationImpl processEngineConfiguration) {
    this.processEngineConfiguration = processEngineConfiguration;
  }
 
  @Override
  public RepositoryService getRepositoryService() {
    return repositoryService;
  }
 
  public void setRepositoryService(RepositoryService repositoryService) {
    this.repositoryService = repositoryService;
  }
 
  @Override
  public RuntimeService getRuntimeService() {
    return runtimeService;
  }
 
  public void setRuntimeService(RuntimeService runtimeService) {
    this.runtimeService = runtimeService;
  }
 
  @Override
  public TaskService getTaskService() {
    return taskService;
  }
 
  public void setTaskService(TaskService taskService) {
    this.taskService = taskService;
  }
 
  @Override
  public HistoryService getHistoryService() {
    return historyService;
  }
 
  public void setHistoryService(HistoryService historyService) {
    this.historyService = historyService;
  }
 
  /**
   * @see #setHistoryService(HistoryService)
   * @param historicService
   *          the historiy service instance
   */
  public void setHistoricDataService(HistoryService historicService) {
    this.setHistoryService(historicService);
  }
 
  @Override
  public IdentityService getIdentityService() {
    return identityService;
  }
 
  public void setIdentityService(IdentityService identityService) {
    this.identityService = identityService;
  }
 
  @Override
  public ManagementService getManagementService() {
    return managementService;
  }
 
  @Override
  public AuthorizationService getAuthorizationService() {
    return authorizationService;
  }
 
  public void setAuthorizationService(AuthorizationService authorizationService) {
    this.authorizationService = authorizationService;
  }
 
  @Override
  public CaseService getCaseService() {
    return caseService;
  }
 
  public void setCaseService(CaseService caseService) {
    this.caseService = caseService;
  }
 
  @Override
  public FormService getFormService() {
    return formService;
  }
 
  public void setFormService(FormService formService) {
    this.formService = formService;
  }
 
  public void setManagementService(ManagementService managementService) {
    this.managementService = managementService;
  }
 
  @Override
  public FilterService getFilterService() {
    return filterService;
  }
 
  public void setFilterService(FilterService filterService) {
    this.filterService = filterService;
  }
 
  @Override
  public ExternalTaskService getExternalTaskService() {
    return externalTaskService;
  }
 
  public void setExternalTaskService(ExternalTaskService externalTaskService) {
    this.externalTaskService = externalTaskService;
  }
 
  @Override
  public DecisionService getDecisionService() {
    return decisionService;
  }
 
  public void setDecisionService(DecisionService decisionService) {
    this.decisionService = decisionService;
  }
 
  public void manageDeployment(org.camunda.bpm.engine.repository.Deployment deployment) {
    this.additionalDeployments.add(deployment.getId());
  }
 
}

具体而言,在ProcessEngineRule类中,继承TestWatcher类,在重写的starting函数,提取单元测试函数的信息,通过TestHelper工具类自动提取@Deployment的注解,并完成单元测试自动部署。

  @Override
  public void starting(Description description) {
    // 获得单元测试函数名
    String methodName = description.getMethodName();
    if (methodName != null) {
      // cut off method variant suffix "[variant name]" for parameterized tests
      int methodNameVariantStart = description.getMethodName().indexOf('[');
      int methodNameEnd = methodNameVariantStart < 0 ? description.getMethodName().length() : methodNameVariantStart;
      methodName = description.getMethodName().substring(0, methodNameEnd);
    }
    // 提取单元测试函数上的@Deployment注解,然后自动部署
    deploymentId = TestHelper.annotationDeploymentSetUp(processEngine, description.getTestClass(), methodName,
        description.getAnnotation(Deployment.class));
  }

在在重写的finished函数中,完成单元测试的清理工作

  @Override
  public void finished(Description description) {
    // 清除认证
    identityService.clearAuthentication();
    processEngine.getProcessEngineConfiguration().setTenantCheckEnabled(true);
 
    // 单元测试完成后自动清除部署文件
    TestHelper.annotationDeploymentTearDown(processEngine, deploymentId, description.getTestClass(), description.getMethodName());
    for (String additionalDeployment : additionalDeployments) {
      TestHelper.deleteDeployment(processEngine, additionalDeployment);
    }
 
    // 自动的清除公共审批意见
    TestHelper.deleteCommonRemarkAll(processEngine);
 
    // 确保数据库已清理干净
    if (ensureCleanAfterTest) {
      TestHelper.assertAndEnsureCleanDbAndCache(processEngine);
    }
 
    // 重置ID生成器
    TestHelper.resetIdGenerator(processEngineConfiguration);
    
    // 重置时间
    ClockUtil.reset();
 
    // 将流程引擎各类的Service全部清空
    clearServiceReferences();
 
    // 清除检测
    PlatformTelemetryRegistry.clear();
  }