桂花流程引擎技术开发系列之测试篇

333 阅读5分钟

篇章引言

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

在桂云网络公司开发桂花流程引擎的过程中,单元测试是一个无法避免的基础知识,今天将单元测试的基础知识点整理一遍。

一、数据库配置

在test目录下,找到一个camunda.cfg.xml的配置文件,里面包含了流程引擎系统配置参数,如以下文件内容:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans.xsd">
  <bean id="processEngineConfiguration" class="org.camunda.bpm.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
    <property name="jdbcUrl" value="jdbc:h2:mem:DatabaseTablePrefixTest;DB_CLOSE_DELAY=1000;" />
    <property name="jdbcDriver" value="org.h2.Driver" />
    <property name="jdbcUsername" value="sa" />
    <property name="jdbcPassword" value="" />
    <property name="databaseSchemaUpdate" value="true" />
    <property name="beans">
      <map/>
    </property>
    <property name="jobExecutorActivate" value="false" />
    <property name="bpmnStacktraceVerbose" value="true" />
    <property name="dbMetricsReporterActivate" value="false" />
    <property name="telemetryReporterActivate" value="false" />
    <property name="taskMetricsEnabled" value="false" />
    <property name="mailServerPort" value="25" />
    <property name="history" value="full" />
    <property name="authorizationCheckRevokes" value="true"/>
    <property name="jdbcBatchProcessing" value="true"/>
    <property name="telemetryEndpoint" value="http://localhost:8081/pings"/>
    <property name="enforceHistoryTimeToLive" value="false" />
  </bean>
</beans>

在单元测试中,主要涉及数据库的配置,单元测试一般采用h2内存数据库,这样可以使流程引擎的部署和单元测试更方便。其中jdbcUrl为数据库的连接地址,默认配置的是jdbc:h2:mem:DatabaseTablePrefixTest;DB_CLOSE_DELAY=1000; 数据库驱动jdbcDriver默认配置为org.h2.Driver,h2数据默认账号为sa,无需配置密码。如果使用其他类型的数据库,则需要配置相应的参数,例如当使用PostgreSQL数据库时,配置参数为

<property name="jdbcUrl" value="jdbc:postgresql://localhost:5432/osg-bpmn-engine" />
<property name="jdbcDriver" value="org.postgresql.Driver" />
<property name="jdbcUsername" value="postgres" />
<property name="jdbcPassword" value="password" />

这是很重要的配置,如果没有配置好数据库,则在单元测试中,无法初始化流程引擎,单元测试无法进行。

二、日志配置

如果对日志有要求,则可以在test目录内找到logback-test.xml的配置文件,其内容一般为

<configuration>
 
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>
 
  <logger name="org.apache.ibatis" level="debug" />
 
  <!-- MyBatis 和 SQL 日志 -->
  <logger name="org.apache.ibatis" level="DEBUG" />
  <logger name="java.sql.Connection" level="DEBUG" />
  <logger name="java.sql.Statement" level="DEBUG" />
  <logger name="java.sql.PreparedStatement" level="DEBUG" />
 
  <logger name="org.mybatis.logging" level="DEBUG" />
  <logger name="javax.activation" level="info" />
  <logger name="org.springframework" level="DEBUG" />
  <logger name="org.mortbay.log" level="info" />
  <logger name="org.camunda" level="info" />
  <logger name="org.camunda.bpm.engine.test" level="debug" />
  <logger name="org.testcontainers" level="info" />
  <logger name="com.github" level="info" />
  <logger name="org.camunda.bpm.engine.impl.persistence.entity" level="debug" />
  <logger name="org.camunda.bpm.engine.impl.persistence.entity.JobEntity" level="debug" />
  <logger name="org.camunda.bpm.engine.impl.persistence.entity.HistoricJobLogEntity" level="debug" />
  <logger name="org.camunda.bpm.engine.history" level="debug" />
  <logger name="org.camunda.bpm.engine.bpmn.parser" level="debug" />
  <logger name="org.camunda.bpm.engine.bpmn.behavior" level="debug" />
  <logger name="org.camunda.bpm.engine.cmmn.transformer" level="debug" />
  <logger name="org.camunda.bpm.engine.cmmn.behavior" level="debug" />
  <logger name="org.camunda.bpm.engine.cmmn.operation" level="debug" />
  <logger name="org.camunda.bpm.engine.cmd" level="debug" />
  <logger name="org.camunda.bpm.engine.persistence" level="debug" />
  <logger name="org.camunda.bpm.engine.tx" level="debug" />
  <logger name="org.camunda.bpm.engine.cfg" level="debug" />
  <logger name="org.camunda.bpm.engine.jobexecutor" level="debug" />
  <logger name="org.camunda.bpm.engine.context" level="debug" />
  <logger name="org.camunda.bpm.engine.core" level="debug" />
  <logger name="org.camunda.bpm.engine.pvm" level="debug" />
  <logger name="org.camunda.bpm.engine.metrics" level="debug" />
  <logger name="org.camunda.bpm.engine.util" level="debug" />
  <logger name="org.camunda.bpm.engine.telemetry" level="debug" />
  <logger name="org.camunda.bpm.application" level="debug" />
  <logger name="org.camunda.bpm.container" level="debug" />
 
  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
 
</configuration>

由上面文件可以看出,可以配置各个模块的日志等级,Camunda也是用slf4j日志插件,其日志等级与slf4j是相同的,按需配置即可。

三、单元测试的工作原理

Camunda单元测试使用的是junit单元测试,因此Camunda单元测试原理与junit相同。Camunda自定义了很多跟单元测试相关的测试工具类,在此仅说明具有代表性的工具类

Deployment注解类。该类主要注解在单元测试的函数中,该测试的函数对应单元测试部署的资源文件。

package org.camunda.bpm.engine.test;
 
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
 
@Retention(RetentionPolicy.RUNTIME)
public @interface Deployment {
  public String[] resources() default {};
}

TestHelper单元测试工具类,该工具类包含很多跟测试相关的工具函数,比如跟以上Deployment注解类相关的解析处理,其过程为,将junit传递的testClass测试类和测试方法methodName传入该函数,然后判断该测试函数是否注解有Deployment类,如没有,则递归寻找父级类,直至找到Deployment注解类,将提取到的信息传入重载函数annotationDeploymentSetUp

  public static String annotationDeploymentSetUp(ProcessEngine processEngine, Class<?> testClass, String methodName,
      Deployment deploymentAnnotation, Class<?>... parameterTypes) {
    Method method = null;
    boolean onMethod = true;
 
    try {
      method = getMethod(testClass, methodName, parameterTypes);
    } catch (Exception e) {
      if (deploymentAnnotation == null) {
        // we have neither the annotation, nor can look it up from the method
        return null;
      }
    }
 
    if (deploymentAnnotation == null) {
      deploymentAnnotation = method.getAnnotation(Deployment.class);
    }
    // if not found on method, try on class level
    if (deploymentAnnotation == null) {
      onMethod = false;
      Class<?> lookForAnnotationClass = testClass;
      while (lookForAnnotationClass != Object.class) {
        deploymentAnnotation = lookForAnnotationClass.getAnnotation(Deployment.class);
        if (deploymentAnnotation != null) {
          testClass = lookForAnnotationClass;
          break;
        }
        lookForAnnotationClass = lookForAnnotationClass.getSuperclass();
      }
    }
 
    if (deploymentAnnotation != null) {
      String[] resources = deploymentAnnotation.resources();
      LOG.debug("annotation @Deployment creates deployment for {}.{}", ClassNameUtil.getClassNameWithoutPackage(testClass), methodName);
      return annotationDeploymentSetUp(processEngine, resources, testClass, onMethod, methodName);
 
    } else {
      return null;
 
    }
  }

重载函数annotationDeploymentSetUp

  public static String annotationDeploymentSetUp(ProcessEngine processEngine, String[] resources, Class<?> testClass,
      boolean onMethod, String methodName) {
    if (resources != null) {
      if (resources.length == 0 && methodName != null) {
        String name = onMethod ? methodName : null;
        String resource = getBpmnProcessDefinitionResource(testClass, name);
        resources = new String[]{resource};
      }
 
      DeploymentBuilder deploymentBuilder = processEngine.getRepositoryService()
        .createDeployment()
        .name(ClassNameUtil.getClassNameWithoutPackage(testClass)+"."+methodName);
 
      for (String resource: resources) {
        deploymentBuilder.addClasspathResource(resource);
      }
 
      return deploymentBuilder.deploy().getId();
    }
 
    return null;
  }

该重载函数在主要处理,将测试函数名或者测试类,通过getBpmnProcessDefinitionResource获得的bpmn资源文件名,并通过RepositoryService创建部署,从而完成将Deployment注解到测试函数的部署文件加入单元测试。

四、流程引擎测试初始化过程

在单元测试中,ProcessEngine是如何初始化的呢?Camunda由junit的TestCase类扩展了ProcessEngineTestCase类,由该类的setUp函数看到,初始化时,由TestHelper工具类自动化部署,获得部署deploymentId,从而完成单元测试自动化部署。在单元测试的tearDown函数,则将该部署自动移除。

package org.camunda.bpm.engine.test;
 
import java.io.FileNotFoundException;
import java.util.Date;
 
import org.camunda.bpm.engine.AuthorizationService;
import org.camunda.bpm.engine.CaseService;
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.ProcessEngineConfiguration;
import org.camunda.bpm.engine.RepositoryService;
import org.camunda.bpm.engine.RuntimeService;
import org.camunda.bpm.engine.TaskService;
import org.camunda.bpm.engine.impl.test.ProcessEngineAssert;
import org.camunda.bpm.engine.impl.test.TestHelper;
import org.camunda.bpm.engine.impl.util.ClockUtil;
 
import junit.framework.TestCase;
 
 
public class ProcessEngineTestCase extends TestCase {
 
  protected String configurationResource = "camunda.cfg.xml";
  protected String configurationResourceCompat = "activiti.cfg.xml";
  protected String deploymentId = null;
 
  protected ProcessEngine processEngine;
  protected RepositoryService repositoryService;
  protected RuntimeService runtimeService;
  protected TaskService taskService;
  @Deprecated protected HistoryService historicDataService;
  protected HistoryService historyService;
  protected IdentityService identityService;
  protected ManagementService managementService;
  protected FormService formService;
  protected FilterService filterService;
  protected AuthorizationService authorizationService;
  protected CaseService caseService;
 
  protected boolean skipTest = false;
 
  /** uses 'camunda.cfg.xml' as it's configuration resource */
  public ProcessEngineTestCase() {
  }
 
  public void assertProcessEnded(final String processInstanceId) {
    ProcessEngineAssert.assertProcessEnded(processEngine, processInstanceId);
  }
 
  @Override
  protected void setUp() throws Exception {
    super.setUp();
 
    if (processEngine==null) {
      initializeProcessEngine();
      initializeServices();
    }
 
    boolean hasRequiredHistoryLevel = TestHelper.annotationRequiredHistoryLevelCheck(processEngine, getClass(), getName());
    // ignore test case when current history level is too low
    skipTest = !hasRequiredHistoryLevel;
 
    if (!skipTest) {
      deploymentId = TestHelper.annotationDeploymentSetUp(processEngine, getClass(), getName());
    }
  }
 
  @Override
  protected void runTest() throws Throwable {
    if (!skipTest) {
      super.runTest();
    }
  }
 
  protected void initializeProcessEngine() {
    try {
      processEngine = TestHelper.getProcessEngine(getConfigurationResource());
    } catch (RuntimeException ex) {
      if (ex.getCause() != null && ex.getCause() instanceof FileNotFoundException) {
        processEngine = ProcessEngineConfiguration
            .createProcessEngineConfigurationFromResource(configurationResourceCompat)
            .buildProcessEngine();
      } else {
        throw ex;
      }
    }
  }
 
  protected void initializeServices() {
    repositoryService = processEngine.getRepositoryService();
    runtimeService = processEngine.getRuntimeService();
    taskService = processEngine.getTaskService();
    historicDataService = processEngine.getHistoryService();
    historyService = processEngine.getHistoryService();
    identityService = processEngine.getIdentityService();
    managementService = processEngine.getManagementService();
    formService = processEngine.getFormService();
    filterService = processEngine.getFilterService();
    authorizationService = processEngine.getAuthorizationService();
    caseService = processEngine.getCaseService();
  }
 
  @Override
  protected void tearDown() throws Exception {
    TestHelper.annotationDeploymentTearDown(processEngine, deploymentId, getClass(), getName());
 
    ClockUtil.reset();
 
    super.tearDown();
  }
 
  public static void closeProcessEngines() {
    TestHelper.closeProcessEngines();
  }
 
  public void setCurrentTime(Date currentTime) {
    ClockUtil.setCurrentTime(currentTime);
  }
 
  public String getConfigurationResource() {
    return configurationResource;
  }
 
  public void setConfigurationResource(String configurationResource) {
    this.configurationResource = configurationResource;
  }
 
}