跳到主要内容

Flowable CMMN API

Doubao 中英对照 The Flowable CMMN API

流程 CMMN 引擎 API 与服务​

引擎 API 是与 Flowable 交互最常用的方式。主要的入口是 CmmnEngine,它可以通过配置小节中所述的几种方式创建。从 CmmnEngine 可以获取包含用例/CMMN 方法的各种服务。CmmnEngine 和这些服务对象都是线程安全的,因此你可以在整个服务器期间持有其中某一个的引用。

CmmnEngine cmmnEngine = CmmnEngineConfiguration.createStandaloneCmmnEngineConfiguration();

CmmnRuntimeService runtimeService = cmmnEngine.getCmmnRuntimeService();
CmmnRepositoryService repositoryService = cmmnEngine.getCmmnRepositoryService();
CmmnTaskService taskService = cmmnEngine.getCmmnTaskService();
CmmnManagementService managementService = cmmnEngine.getCmmnManagementService();
CmmnHistoryService historyService = cmmnEngine.getCmmnHistoryService();

CmmnEngineConfiguration.createStandaloneCmmnEngineConfiguration() 会初始化并构建一个 CMMN 引擎,之后总是返回该 CMMN 引擎。

CmmnEngineConfiguration 类会扫描所有 flowable.cmmn.cfg.xml 和 flowable-cmmn-context.xml 文件。对于所有 flowable.cmmn.cfg.xml 文件,CMMN 引擎将以典型的 Flowable 方式构建:CmmnEngineConfiguration.createCmmnEngineConfigurationFromInputStream(inputStream).buildCmmnEngine()。对于所有 flowable-cmmn-context.xml 文件,CMMN 引擎将以 Spring 方式构建:先创建 Spring 应用上下文,然后从该应用上下文中获取 CMMN 引擎。

所有服务都是无状态的。这意味着你可以轻松地在集群中的多个节点上运行 Flowable,每个节点都连接同一个数据库,而无需担心之前的调用实际是在哪台机器上执行的。对任何服务的任何调用都是幂等的,无论它在哪里执行。

CmmnRepositoryService 大概是使用 Flowable CMMN 引擎时最先需要的服务。该服务提供用于管理和操作部署与用例定义的操作。这里不展开细讲:用例定义是 CMMN 1.1 case 在 Java 中的对应物,它表示一个用例中各个步骤的结构和行为。部署是 Flowable CMMN 引擎中的打包单元。一个部署可以包含多个 CMMN 1.1 XML 文件以及任意其他资源。一个部署中包含什么由开发者决定。它可以只包含单个流程 CMMN 1.1 XML 文件,也可以是一整包用例及相关资源(例如,名为 'hr-cases' 的部署可以包含与 HR 用例相关的一切)。CmmnRepositoryService 可以部署这样的包。部署一个部署,意味着将其上传到引擎,在引擎中所有用例都会被检查和解析,然后存入数据库。从那时起,该部署就为系统所知,部署中包含的任何流程都可以被启动。

此外,该服务还允许你:

  • 查询引擎已知的部署和用例定义。
  • 获取各种资源,例如部署中包含的文件,或由引擎自动生成的用例图。
  • 获取用例定义的 POJO 版本,它可以用来以 Java 而非 XML 的方式内省用例。

CmmnRepositoryService 主要处理静态信息(不会改变、或至少不常改变的数据),而 CmmnRuntimeService 则恰好相反。它负责启动用例定义的新用例实例。如前所述,用例定义定义了一个用例中各个步骤的结构和行为。用例实例就是这样一个用例定义的一次执行。对于每个用例定义,通常会同时运行许多实例。CmmnRuntimeService 也是用于获取和存储用例变量的服务。这些数据是特定于给定用例实例的,可以被用例中的各种结构使用(例如,计划转换条件常常用流程变量来决定选择哪条路径继续用例)。CmmnRuntimeservice 还允许你查询用例实例和计划项。计划项是 CMMN 1.1 中已启用计划项的表示。最后,每当用例实例正在等待外部触发、需要继续推进用例时,都会使用 CmmnRuntimeService。一个用例实例可以有多种等待状态,该服务包含各种操作,用于向实例"发出信号",告知外部触发已收到、用例实例可以继续推进。

需要由系统人类用户执行的任务,是像 Flowable 这样的 CMMN 引擎的核心。围绕任务的一切都归拢在 CmmnTaskService 中,例如:

  • 查询分配给用户或组的任务。
  • 创建新的独立任务。这些是与流程实例无关的任务。
  • 操作任务分配给哪个用户,或哪些用户以某种方式参与了该任务。
  • 认领和完成任务。认领意味着某人决定成为该任务的办理人,即该用户将完成此任务。完成意味着"做任务的工作"。通常这就是填写某种表单。

CmmnHistoryService 暴露 Flowable CMMN 引擎收集的所有历史数据。在执行用例时,引擎可以保留大量数据(这是可配置的),例如用例实例的启动时间、谁做了哪些任务、完成任务花了多长时间、每个用例实例走了哪条路径等等。该服务主要暴露查询能力来访问这些数据。

CmmnManagementService 提供对数据库表底层信息的访问,允许查询不同类型的作业并执行它们。

有关服务操作和引擎 API 的更详细信息,请参见 javadocs。

异常策略​

Flowable 中的基础异常是 org.flowable.engine.common.api.FlowableException,一个非受检异常。该异常随时可能由 API 抛出,但在特定方法中发生的"预期"异常会记录在 javadocs 中。例如,以下是 CmmnTaskService 的一段摘录:

/**
* Called when the task is successfully executed.
* @param taskId the id of the task to complete, cannot be null.
* @throws FlowableObjectNotFoundException when no task exists with the given id.
*/
void complete(String taskId);

在上面的例子中,当传入一个不存在任务的 id 时,就会抛出异常。此外,由于 javadoc 明确说明 taskId 不能为 null,传入 null 时将抛出 FlowableIllegalArgumentException。

尽管我们希望避免庞大的异常层次结构,但以下子类会在特定情况下抛出。在流程执行或 API 调用期间发生的、不符合下列异常的所有其他错误,都作为普通的 FlowableException 抛出。

  • FlowableWrongDbException:当 Flowable 引擎发现数据库 schema 版本与引擎版本不匹配时抛出。
  • FlowableOptimisticLockingException:当数据存储中因同一数据条目被并发访问而发生乐观锁冲突时抛出。
  • FlowableClassLoadingException:当请求加载的类未找到、或在加载过程中发生错误时抛出(例如 JavaDelegates、TaskListeners 等)。
  • FlowableObjectNotFoundException:当被请求或被操作的对象不存在时抛出。
  • FlowableIllegalArgumentException:表示在 Flowable API 调用中提供了非法参数、在引擎配置中配置了非法值、提供了非法值,或在流程定义中使用了非法值。
  • FlowableTaskAlreadyClaimedException:当任务已被认领、又调用 taskService.claim(...) 时抛出。

查询 API​

从引擎查询数据有两种方式:查询 API 和原生查询。查询 API 让你可以用流畅的 API 编写完全类型安全的查询。你可以为查询添加各种条件(所有条件以逻辑 AND 的方式共同生效)以及恰好一个排序。以下代码展示了一个示例:

List<Task> tasks = taskService.createTaskQuery()
.taskAssignee("kermit")
.orderByDueDate().asc()
.list();

变量​

每个用例实例都需要并使用数据来执行构成它的各个步骤。在 Flowable 中,这种数据称为变量,存储在数据库中。变量可以用于表达式中(例如哨兵的条件),用于 Java 服务任务调用外部服务时(例如提供输入或存储服务调用的结果),等等。

用例实例可以有变量(称为用例变量),计划项实例和人工任务也可以有变量。一个用例实例可以有任意数量的变量。每个变量存储在 ACT_RU_VARIABLE 数据库表的一行中。

createCaseInstanceBuilder 方法提供了可选方法,用于在通过 CmmnRuntimeService 创建并启动用例实例时提供变量:

CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().variable("var1", "test").start();

变量可以在用例执行期间添加。例如(CmmnRuntimeService):

void setVariables(String caseInstanceId, Map<String, ? extends Object> variables);

变量也可以获取,如下所示。注意,CmmnTaskService 上也有类似的方法。

Map<String, Object> getVariables(String caseInstanceId);
Object getVariable(String caseInstanceId, String variableName);

变量经常用于 Java 服务任务、表达式、脚本等等。

瞬态变量​

瞬态变量是行为与普通变量相似、但不会被持久化的变量。瞬态变量通常用于高级场景。不确定时,请使用普通的用例变量。

瞬态变量遵循以下规则:

  • 瞬态变量完全不存储任何历史记录。
  • 与普通变量一样,瞬态变量在设置时会被放到最高层父级上。这意味着当在某个计划项上设置变量时,该瞬态变量实际上存储在用例实例执行上。与普通变量一样,如果变量设置在特定的计划项或任务上,也存在该方法的 local 变体。
  • 瞬态变量只能在用例定义的下一个"等待状态"之前访问。此后它们就消失了。这里的等待状态,指用例实例被持久化到数据存储的那个点。
  • 瞬态变量只能通过 setTransientVariable(name, value) 设置,但调用 getVariable(name) 时也会返回瞬态变量(getTransientVariable(name) 也存在,它只检查瞬态变量)。这样做的原因是为了便于编写表达式,让现有的使用变量的逻辑对两种类型都适用。
  • 瞬态变量会遮蔽同名的持久变量。这意味着当用例实例上同时设置了持久变量和瞬态变量、并调用 getVariable("someVariable") 时,将返回瞬态变量的值。

在大多数暴露普通变量的地方,你都可以设置和获取瞬态变量:

  • 在 PlanItemJavaDelegate 实现中的 DelegatePlanItemInstance 上
  • 通过运行时服务启动用例实例时
  • 完成任务时

这些方法遵循普通用例变量的命名约定:

CaseInstance caseInstance = runtimeService.createCaseInstanceBuilder().transientVariable("var1", "test").start();

表达式​

Flowable 使用 UEL 进行表达式解析。UEL 代表统一表达式语言(Unified Expression Language),是 EE6 规范的一部分(详细信息请参见 EE6 规范)。

表达式可用于例如 Java 服务任务、哨兵条件和计划项监听器。虽然表达式分为值表达式和方法表达式两种类型,但 Flowable 对此做了抽象,因此在需要表达式的地方两者都可以使用。

  • 值表达式:解析为一个值。默认情况下,所有用例变量都可使用。此外,所有 Spring bean(如果使用了 Spring)也可在表达式中使用。在非 Spring 环境中,可供表达式使用的 bean 可以通过 CmmnEngineConfiguration 的 setBeans 方法设置。示例:
${myVariable}
${myBean.myProperty}
  • 方法表达式:调用一个带或不带参数的方法。调用无参数的方法时,请务必在方法名后加上空括号(以便将该表达式与值表达式区分开来)。 传入的参数可以是字面值,也可以是会自行解析的表达式。示例:
${printer.print()}
${myBean.addNewOrder('orderName')}
${myBean.doSomething(myVar, planItemInstance)}

这些表达式支持解析基本类型(包括比较)、bean、列表、数组和 map。

除了所有用例实例变量之外,还有一些默认可用于表达式的对象:

  • caseInstance:保存关于当前进行中的用例实例的附加信息。caseInstance 关键字在所有表达式中都可用。
  • planItemInstance:DelegatePlanItemInstance 保存关于当前计划项实例的附加信息。planItemInstance 关键字在所有与计划项相关的表达式中可用(例如哨兵条件、计划项生命周期监听器、服务任务表达式等)。
  • planItemInstances:暴露关于所有当前计划项实例的信息。有关如何使用它,请参见下面的示例。
  • variableContainer:当前表达式正在为之解析的 VariableContainer。变量容器是对用例实例、计划项实例、流程实例和执行的一层抽象。+variableContainer 关键字让你可以编写不绑定到特定实现的表达式。
  • authenticatedUserId:当前已认证用户的 id。如果没有用户被认证,则该变量不可用。

例如:

${caseInstance.id}
${caseInstance.getVariable('myVariable') == 'test'}
${caseInstance.setVariable('myVariable', 'test')}

${planItemInstance.getPlanItem().getPlanItemDefinition().getName()}
${planItemInstance.getVariable('myVariable') == 123}
${planItemInstance.setVariable('myVariable', 123)}

${variableContainer.getVariable('myVariable')}
${variableContainer.setVariable('myVariable', 'true')}

planItemInstances 关键字需要稍作解释。使用该关键字可以检索所有当前计划项实例,同时它还充当一种 API,用于获取关于当前计划项实例的更多信息。

例如,以下表达式:

${planItemInstances.active().count()}

将返回当前处于 'active' 状态的所有计划项实例的数量。如果只关心某个计划项,可以进行过滤:

${planItemInstances.definitionId('a').active().count()}

如示例所示,planItemInstances 关键字允许将各种过滤方法链式串联。这种串联采用 AND 语义。支持以下方法。

按计划项实例状态过滤的方法:

  • active()
  • available()
  • enabled()
  • disabled()
  • completed()
  • terminated()

以下状态过滤器也受支持,但请注意它们反映的是不符合规范的内部状态:

  • unavailable()
  • waitingForRepetition()
  • asyncActive()

要获取处于终态(即终止或完成)或非终态的所有计划项实例:

  • onlyTerminal()
  • onlyNonTerminal()

要根据 CMMN 模型中设置的标识进行过滤:

  • definitionId('id1')
  • definitionIds('id1', 'id2')

要根据名称进行过滤:

  • name('name1')
  • names('name1', 'name2', 'name3')

要使用 lambda 表达式进行过滤:

  • filter(planItemInstance -> planItemInstance.name.startsWith('Service')

对于某些场景,应过滤的计划项实例应只属于当前阶段。"当前阶段"要么是某个计划项的父阶段,要么是在没有父阶段时的用例实例。

  • currentStage()

最后,有一组方法不能被链式串联,因为它们会返回结果。

  • count():应用所有过滤器后,返回计划项实例的数量。
  • getDefinitionIds():返回一个字符串列表,包含匹配的计划项实例在 CMMN 模型中定义的所有 id。
  • getDefinitionNames():返回一个名称列表,包含匹配的计划项实例在 CMMN 模型中定义的所有名称。
  • getList():返回 org.flowable.cmmn.api.runtime.PlanItemInstance 实例的"原始"列表。

下面看几个在表达式中使用上述方法的例子:

统计一个用例实例中所有活动计划项实例的数量:

${planItemInstances.active().count()}

统计 id 为 'a' 或 'b' 的所有活动计划项实例的数量:

${planItemInstances.active().definitionIds('a', 'b').count()}

获取当前阶段中处于终态的所有计划项实例的 id:

${planItemInstances.currentStage().onlyTerminal().getDefinitionIds()()}

将上述表达式的结果存入一个瞬态变量

${caseInstance.setTransientVariable('myVar', planItemInstances.currentStage().onlyTerminal().getDefinitionIds()}}

表达式函数​

[实验性] 表达式函数在 6.4.0 版本中加入。

为了更方便地操作用例变量,variables 命名空间下提供了一组开箱即用的函数。

  • variables:get(varName):获取一个变量的值。与直接在表达式中写变量名相比,主要区别在于使用该函数时变量不存在也不会抛出异常。例如,如果 myVariable 不存在,${myVariable == "hello"} 会抛出异常,而 ${var:get(myVariable) == 'hello'} 则会正常工作。
  • variables:getOrDefault(varName, defaultValue):与 get 类似,但可以提供一个默认值,当变量未设置或值为 null 时返回该默认值。
  • variables:exists(varName):如果变量的值非 null,则返回 true。
  • variables:isEmpty(varName)(别名 :empty):检查变量值是否为空。根据变量类型,行为如下:
-   For String variables, the variable is deemed empty if it’s the empty string.

- For java.util.Collection variables, *true* is returned if the collection has no elements.

- For ArrayNode variables, *true* is returned if there are no elements.

- In case the variable is *null*, *true* is always returned.
  • variables:isNotEmpty(varName)(别名 :notEmpty):isEmpty 的反向操作。
  • variables:equals(varName, value)(别名 :eq):检查变量是否等于给定值。它是这样一个表达式的简写:${execution.getVariable("varName") != null && execution.getVariable("varName") == value}。
    • 如果变量值为 null,则返回 false(除非与 null 比较)。
  • variables:notEquals(varName, value)(别名 :ne):equals 的反向比较。
  • variables:contains(varName, value1, value2, …​):检查所提供的所有值是否都包含在变量中。根据变量类型,行为如下:
-   For String variables, the passed values are used as substrings that need to be part of the variable.

- For java.util.Collection variables, all the passed values need to be an element of the collection (regular *contains* semantics).

- For ArrayNode variables: supports checking if the arraynode contains a JsonNode for the types that are supported as variable type.

- When the variable value is null, false is returned in all cases. When the variable value is not null, and the instance type is not one of the types above, false will be returned.
  • variables:containsAny(varName, value1, value2, …​):与 contains 函数类似,但只要传入的值中有任意一个(而非全部)包含在变量中,就返回 true。
  • variables:base64(varName):将 Binary 或 String 变量转换为 Base64 字符串。
  • 比较器函数:
-   **variables:lowerThan(varName, value)** (alias *:lessThan* or *:lt*) : shorthand for *${execution.getVariable("varName") != null && execution.getVariable("varName") &lt; value}*.

- **variables:lowerThanOrEquals(varName, value)** (alias *:lessThanOrEquals* or *:lte*) : similar, but now for *&lt; =*.

- **variables:greaterThan(varName, value)** (alias *:gt*) : similar, but now for *&gt;*.

- **variables:greaterThanOrEquals(varName, value)** (alias *:gte*) : similar, but now for *&gt; =*.

variables 命名空间有别名 vars 或 var。因此 variables:get(varName) 等价于写 vars:get(varName) 或 var:get(varName)。注意变量名不需要加引号:var:get(varName) 等价于 var:get(\'varName') 或 var:get("varName")。

还要注意,上述函数都不需要把 planItemInstance 或 caseInstance 传入函数(不使用函数时才需要这样做)。引擎在调用函数时会注入合适的变量作用域。这也意味着在 BPMN 流程定义中编写表达式时,可以以完全相同的方式使用这些函数。

这些变量函数在 CMMN 中特别有用,例如在编写哨兵 if 部分的条件时。以如下 CMMN 用例定义为例:

cmmn.expression functions

假设该哨兵除了完成事件之外还有一个 if 部分。在用例实例刚启动后,这个 if 部分的条件就会被求值(因为阶段变为可用)。如果条件形如 ${someVariable == someValue},这意味着该变量需要在用例实例启动时可用。在很多情况下这并不现实,或者变量之后才出现(例如来自表单),这会导致底层的 PropertyNotFoundException。考虑到可能为 null,正确的表达式就必须写成:

${planItemInstance.getVariable('someVariable') != null && planItemInstance.getVariable('someVariable') == someValue}

这相当冗长。然而使用上面的函数,可以简化为:

${var:eq(someVariable, someValue)}

或者:

${var:get(someVariable) == someValue}

函数实现会考虑变量可能为 null 的情况(变量为 null 时不会抛出异常),并正确处理相等性判断。

此外,还可以注册可在表达式中使用的自定义函数。更多信息请参见 org.flowable.common.engine.api.delegate.FlowableFunctionDelegate 接口。

历史数据清理​

默认情况下历史数据会永久存储,这可能导致历史表变得非常庞大,并影响 HistoryService 的性能。历史清理功能在 6.5.0 版本引入,允许删除 HistoricProcessInstance 及其关联数据。一旦流程数据不再需要保留,就可以将其删除,以减小历史数据库的大小。

自动历史清理配置​

历史用例实例的自动清理默认是禁用的,但可以通过编程方式启用和配置。启用后,默认在凌晨 1 点运行一个清理作业,删除所有在 365 天前或更早结束的 HistoricCaseInstance 及关联数据。历史流程的删除通过 Flowable 批处理机制完成:调度作业分批删除流程,并将所做操作的信息存储在批处理表中。

CmmnEngine cmmnEngine = CmmnEngineConfiguration.
.createProcessEngineConfigurationFromResourceDefault()
.setEnableHistoryCleaning(true)
.setHistoryCleaningTimeCycleConfig("0 0 1 * * ?")
.setCleanInstancesEndedAfter(Duration.ofDays(365))
.buildCmmnEngine();

在 application.properties 或外部化配置中设置的 Spring 属性也可以使用:

 flowable.enable-history-cleaning=true
flowable.history-cleaning-after=365d
flowable.history-cleaning-cycle=0 0 1 * * ?

手动删除历史数据​

手动清理历史可以通过在 CmmnHistoryService 查询构建器上执行方法来完成。

删除所有超过一年的 HistoricCaseInstance 及其相关数据。

 int numberOfCasesInBatch = 10;
Calendar cal = new GregorianCalendar();
cal.set(Calendar.YEAR, cal.get(Calendar.YEAR) - 1);
cmmnHistoryService.createHistoricCaseInstanceQuery()
.finishedBefore(cal.getTime())
.deleteSequentiallyUsingBatch(numberOfCasesInBatch, "Custom Delete Batch");

单元测试​

用例是软件项目不可或缺的一部分,它应该像测试普通应用逻辑一样通过单元测试来测试。由于 Flowable 是一个可嵌入的 Java 引擎,为业务用例编写单元测试就像编写普通单元测试一样简单。

Flowable 支持 JUnit Jupiter 风格的单元测试。

在 JUnit Jupiter 风格下,需要使用 org.flowable.cmmn.engine.test.FlowableCmmnTest 注解,或者手动注册 org.flowable.cmmn.engine.test.FlowableCmmnExtension。FlowableCmmnTest 注解只是一个元注解,它会完成 FlowableCmmnExtension 的注册(即它做了 @ExtendWith(FlowableCmmnExtension.class))。这会让 CmmnEngine 和各服务作为参数传入测试方法和生命周期方法(@BeforeAll、@BeforeEach、@AfterEach、@AfterAll)。在每个测试之前,cmmnEngine 默认会用 classpath 上的 flowable.cmmn.cfg.xml 资源初始化。要指定不同的配置文件,需要使用 org.flowable.cmmn.engine.test.CmmnConfigurationResource 注解(参见第二个例子)。当配置资源相同时,Cmmn 引擎会在多个单元测试之间被静态缓存。

通过使用 FlowableCmmnExtension,你可以用 org.flowable.cmmn.engine.test.CmmnDeployment 注解测试方法。当测试方法标注了 @CmmnDeployment 时,在每个测试之前会部署 CmmnDeployment#resources 中定义的 cmmn 文件。如果没有定义资源,则会部署与测试类同包下、形如 testClassName.testMethod.cmmn 的资源文件。测试结束时,该部署会被删除,包括所有相关的用例实例、任务等等。更多信息请参见 CmmnDeployment 类。

综合以上,一个 JUnit Jupiter 测试如下所示:

使用默认资源的 JUnit Jupiter 测试。

@FlowableCmmnTest
class MyTest {

private CmmnEngine cmmnEngine;
private CmmnRuntimeService cmmnRuntimeService;
private CmmnTaskService cmmnTaskService;

@BeforeEach
void setUp(CmmnEngine cmmnEngine) {
this.cmmnEngine = cmmnEngine;
this.cmmnRuntimeService = cmmnEngine.getCmmnRuntimeService();
this.cmmnTaskService = cmmnEngine.getTaskRuntimeService();
}

@Test
@CmmnDeployment
void testSingleHumanTask() {
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.caseDefinitionKey("myCase")
.start();
assertNotNull(caseInstance);

Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult();
assertEquals("Task 1", task.getName());
assertEquals("JohnDoe", task.getAssignee());

cmmnTaskService.complete(task.getId());
assertEquals(0, cmmnRuntimeService.createCaseInstanceQuery().count());
}
}

使用 JUnit Jupiter 时,你还可以将部署的 id(通过 +org.flowable.cmmn.engine.test.CmmnDeploymentId+_)注入测试方法和生命周期方法。

使用自定义资源的 JUnit Jupiter 测试。

@FlowableCmmnTest
@CmmnConfigurationResource("flowable.custom.cmmn.cfg.xml")
class MyTest {

private CmmnEngine cmmnEngine;
private CmmnRuntimeService cmmnRuntimeService;
private CmmnTaskService cmmnTaskService;

@BeforeEach
void setUp(CmmnEngine cmmnEngine) {
this.cmmnEngine = cmmnEngine;
this.cmmnRuntimeService = cmmnEngine.getCmmnRuntimeService();
this.cmmnTaskService = cmmnEngine.getTaskRuntimeService();
}
@Test
@CmmnDeployment
void testSingleHumanTask() {
CaseInstance caseInstance = cmmnRuntimeService.createCaseInstanceBuilder()
.caseDefinitionKey("myCase")
.start();
assertNotNull(caseInstance);

Task task = cmmnTaskService.createTaskQuery().caseInstanceId(caseInstance.getId()).singleResult();
assertEquals("Task 1", task.getName());
assertEquals("JohnDoe", task.getAssignee());

cmmnTaskService.complete(task.getId());
assertEquals(0, cmmnRuntimeService.createCaseInstanceQuery().count());
}
}