篇章引言
桂花流程引擎,是基于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;
}
}