桂花流程引擎(Osmanthus)

篇章引言

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

关键字: 桂云网络, 桂云, OSG, 桂花流程引擎, 桂花流程, 桂花, bpmn

在桂云网络公司开发桂花流程引擎的过程中,单元测试是非常重要的一环,因为流程引擎测试必须要走完完整的流程引擎初始化、流程图部署、流程流转到指定状态、流程清除等过程,如果不使用单元测试,那么在开发过程中将会非常耗时。本篇文章主要说明单元测试的工作原理和使用示例。

Camunda单元测试类的继承关系

单元测试,一般使用扩展自PluggableProcessEngineTestCase类,其继承路径为以下关系,直至继承junit的TestCase类

TestCase <- PvmTestCase <- AbstractProcessEngineTestCase <- PluggableProcessEngineTestCase

其中PvmTestCase并没有定义很多接口,只定义了两个断言函数和一个默认手动激活规则。

public class PvmTestCase extends TestCase {

  // 断言是否包含文字
  public void assertTextPresent(String expected, String actual) {
    if ( (actual==null)
         || (actual.indexOf(expected)==-1)
       ) {
      throw new AssertionFailedError("expected presence of ["+expected+"], but was ["+actual+"]");
    }
  }

  // 断言是否包含指定文字,忽略大小写
  public void assertTextPresentIgnoreCase(String expected, String actual) {
    assertTextPresent(expected.toLowerCase(), actual.toLowerCase());
  }

  public Object defaultManualActivation() {
    Expression expression = new FixedValue(true);
    CaseControlRuleImpl caseControlRule = new CaseControlRuleImpl(expression);
    return caseControlRule;
  }
}

再来看AbstractProcessEngineTestCase,忽略非关键函数

public abstract class AbstractProcessEngineTestCase extends PvmTestCase {

  // 流程引擎实例
  protected ProcessEngine processEngine;

  // 流程部署
  protected String deploymentId;
  protected Set<String> deploymentIds = new HashSet<>();

  protected Throwable exception;

  protected ProcessEngineConfigurationImpl processEngineConfiguration;
  
  // 各类的流程引擎service
  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;

  // 初始化流程引擎
  protected abstract void initializeProcessEngine();

  // 关闭流程引擎
  protected void closeDownProcessEngine() {
  }

  // 单元测试用例的逻辑
  @Override
  public void runBare() throws Throwable {
    // 调用初始化流程引擎
    initializeProcessEngine();
    if (repositoryService==null) {
      // 初始化流程引擎的各种service
      initializeServices();
    }

    try {

      // 通过注解来获取历史记录等级
      boolean hasRequiredHistoryLevel = TestHelper.annotationRequiredHistoryLevelCheck(processEngine, getClass(), getName());
      
      // 通过注解获取数据库配置
      boolean runsWithRequiredDatabase = TestHelper.annotationRequiredDatabaseCheck(processEngine, getClass(), getName());
      // ignore test case when current history level is too low or database doesn't match
      if (hasRequiredHistoryLevel && runsWithRequiredDatabase) {

        // 单元测试之前通过@Deployment注解自动部署
        deploymentId = TestHelper.annotationDeploymentSetUp(processEngine, getClass(), getName());

        // 调用单元测试用例的逻辑
        super.runBare();
      }

    }
    catch (AssertionFailedError e) {
      LOG.error("ASSERTION FAILED: " + e, e);
      exception = e;
      throw e;

    }
    catch (Throwable e) {
      LOG.error("EXCEPTION: " + e, e);
      exception = e;
      throw e;

    }
    finally {

      // 测试完毕
      
      // 清除认证
      identityService.clearAuthentication();
      processEngineConfiguration.setTenantCheckEnabled(true);

      // 删除部署
      deleteDeployments();

      // 删除Job
      deleteHistoryCleanupJobs();

      // 断言数据库是否已经清空
      TestHelper.assertAndEnsureCleanDbAndCache(processEngine, exception == null);
      
      // 重置ID生成器
      TestHelper.resetIdGenerator(processEngineConfiguration);
      
      // 重置时间
      ClockUtil.reset();

      // 关闭流程引擎
      closeDownProcessEngine();
      
      // 清除流程引擎的各种service
      clearServiceReferences();
    }
  }

  protected void deleteHistoryCleanupJobs() {
    final List<Job> jobs = historyService.findHistoryCleanupJobs();
    for (final Job job: jobs) {
      processEngineConfiguration.getCommandExecutorTxRequired().execute(new Command<Void>() {
        public Void execute(CommandContext commandContext) {
            commandContext.getJobManager().deleteJob((JobEntity) job);
          return null;
        }
      });
    }
  }

  protected void deleteDeployments() {
    if(deploymentId != null) {
      deploymentIds.add(deploymentId);
    }

    for(String deploymentId : deploymentIds) {
      TestHelper.annotationDeploymentTearDown(processEngine, deploymentId, getClass(), getName());
    }

    deploymentId = null;
    deploymentIds.clear();
  }

  // 初始化流程引擎的各种service
  protected void initializeServices() {
    processEngineConfiguration = ((ProcessEngineImpl) processEngine).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();
  }

  // 清除流程引擎的各种service
  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;
  }
  
  // 其余
}

由以上可以看出,AbstractProcessEngineTestCase只是一个抽象类,不能直接实例化,因此再看PluggableProcessEngineTestCase的源码

public class PluggableProcessEngineTestCase extends AbstractProcessEngineTestCase {

  // 缓存的流程引擎实例
  protected static ProcessEngine cachedProcessEngine;

  // 流程引擎初始化
  protected void initializeProcessEngine() {
    processEngine = getOrInitializeCachedProcessEngine();
  }

  // 流程引擎初始化
  private static ProcessEngine getOrInitializeCachedProcessEngine() {
    if (cachedProcessEngine == null) {
      try {
        cachedProcessEngine = ProcessEngineConfiguration
                .createProcessEngineConfigurationFromResource("camunda.cfg.xml")
                .buildProcessEngine();
      } catch (RuntimeException ex) {
        if (ex.getCause() != null && ex.getCause() instanceof FileNotFoundException) {
          cachedProcessEngine = ProcessEngineConfiguration
              .createProcessEngineConfigurationFromResource("activiti.cfg.xml")
              .buildProcessEngine();
        } else {
          throw ex;
        }
      }
    }
    return cachedProcessEngine;
  }

  // 获取流程引擎实例
  public static ProcessEngine getProcessEngine() {
    return getOrInitializeCachedProcessEngine();
  }
}

该类的只有流程引擎的配置与初始化,配置读取camunda.cfg.xml配置文件,或者如果以上文件不存在,则读取activiti.cfg.xml配置文件,并最终完成初始化获得processEngine实例。

如何使用

先看一个读取流程引擎配置信息的例子,这个测试案例获取流程引擎的数据库类型和日志记录等级。

package com.osgit.bpmn.core;

import org.camunda.bpm.engine.test.ProcessEngineTestCase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyTest extends ProcessEngineTestCase {
    private final static Logger LOGGER = LoggerFactory.getLogger(MyTest.class);

    public void testDatabaseAndHistoryLevel() {
        LOGGER.info("测试数据库类型:{}", this.processEngine.getProcessEngineConfiguration().getDatabaseType());
        LOGGER.info("测试日志记录等级:{}", this.processEngine.getProcessEngineConfiguration().getHistory());
    }
}

直接运行该testDatabaseAndHistoryLevel函数,将得到以下结果,结果是运营正常且正确的

=========================================================================
21:46:18.297 [main] INFO  com.osgit.bpmn.core.MyTest - 测试数据库类型:h2
21:46:18.297 [main] INFO  com.osgit.bpmn.core.MyTest - 测试日志记录等级:full
21:46:18.297 [main] DEBUG org.camunda.bpm.engine.test - annotation @Deployment deletes deployment for MyTest.testDatabaseAndHistoryLevel

再来看一个带测试部署的案例,我们bpmn文件案例存放在java的resources目录内,那么将单元测试的@Deployment注解到单元测试函数上,运行单元测试前,就会自动在本单元测试响应目录寻找MyTest.testDeploymentAndGetTaskName.bpmn20.xml文件,部署后,在单元测试中,使用runtimeService.startProcessInstanceByKey启动流程即可,这样就实现了桂花流程引擎的单元测试

package com.osgit.bpmn.core;

import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.task.Task;
import org.camunda.bpm.engine.test.Deployment;
import org.camunda.bpm.engine.test.ProcessEngineTestCase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyTest extends ProcessEngineTestCase {
    private final static Logger LOGGER = LoggerFactory.getLogger(MyTest.class);

    @Deployment
    public void testDeploymentAndGetTaskName() {

        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("Process_036276n");
        Task task = taskService.createTaskQuery().singleResult();
        
        LOGGER.info("测试任务节点: {}", task.getName());
    }
}

运行结果如下

22:21:30.022 [main] INFO  com.osgit.bpmn.core.MyTest - 测试任务节点: 任务一
22:21:30.023 [main] DEBUG org.camunda.bpm.engine.test - annotation @Deployment deletes deployment for MyTest.testDeploymentAndGetTaskName

作者: 桂云网络, 桂云, OSG

更多推荐