Back to Repositories

Testing BPM Task Candidate Assignment Logic in ruoyi-vue-pro

This test suite validates the BpmTaskCandidateInvoker class functionality in handling task candidate assignments and user calculations within the Flowable BPM framework. It focuses on testing candidate user strategies, task assignments, and user validation scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of the BpmTaskCandidateInvoker’s core functionality:
  • Task candidate calculation scenarios with and without valid users
  • User validation and filtering of disabled users
  • Integration with AdminUserApi and BpmProcessInstanceService
  • Complex scenarios involving start user handling and empty strategy fallbacks

Implementation Analysis

The testing approach employs JUnit with Mockito for robust mocking and verification:
  • Uses MockedStatic for static utility classes (SpringUtil, BpmnModelUtils)
  • Implements spy objects for strategy verification
  • Simulates Flowable BPM model objects and executions
  • Tests both positive and negative scenarios for user assignment

Technical Details

Testing infrastructure leverages:
  • JUnit 5 testing framework
  • Mockito for mocking dependencies
  • BaseMockitoUnitTest as the base test class
  • Custom utility methods for extension element mocking
  • Flowable BPM model objects and interfaces

Best Practices Demonstrated

The test suite exhibits several testing best practices:
  • Proper test setup and cleanup with @BeforeEach
  • Comprehensive mock configurations
  • Clear scenario documentation in test method names
  • Thorough assertion of expected outcomes
  • Effective handling of complex object hierarchies

yunaiv/ruoyi-vue-pro

yudao-module-bpm/yudao-module-bpm-biz/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java

            
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate;

import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.other.BpmTaskCandidateAssignEmptyStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.ExtensionElement;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.ProcessInstance;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Spy;
import org.mockito.internal.util.collections.Sets;

import java.util.*;

import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_NAMESPACE;
import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_PREFIX;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

/**
 * {@link BpmTaskCandidateInvoker} 的单元测试
 *
 * @author 芋道源码
 */
public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {

    private BpmTaskCandidateInvoker taskCandidateInvoker;

    @Mock
    private AdminUserApi adminUserApi;

    @Mock
    private BpmProcessInstanceService processInstanceService;

    @Spy
    private BpmTaskCandidateStrategy userStrategy;
    @Mock
    private BpmTaskCandidateAssignEmptyStrategy emptyStrategy;

    @Spy
    private List<BpmTaskCandidateStrategy> strategyList;

    @BeforeEach
    public void setUp() {
        userStrategy = new BpmTaskCandidateUserStrategy(); // 创建 strategy 实例
        when(emptyStrategy.getStrategy()).thenReturn(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY);
        strategyList = ListUtil.of(userStrategy, emptyStrategy); // 创建 strategyList
        taskCandidateInvoker = new BpmTaskCandidateInvoker(strategyList, adminUserApi);
    }

    /**
     * 场景:成功计算到候选人,但是移除了发起人的用户
     */
    @Test
    public void testCalculateUsersByTask_some() {
        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
            // 准备参数
            String param = "1,2";
            DelegateExecution execution = mock(DelegateExecution.class);
            // mock 方法(DelegateExecution)
            UserTask userTask = mock(UserTask.class);
            String processInstanceId = randomString();
            when(execution.getProcessInstanceId()).thenReturn(processInstanceId);
            when(execution.getCurrentFlowElement()).thenReturn(userTask);
            when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY)))
                    .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString());
            when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM)))
                    .thenReturn(param);
            // mock 方法(adminUserApi)
            AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
                    .setStatus(CommonStatusEnum.ENABLE.getStatus()));
            AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
                    .setStatus(CommonStatusEnum.ENABLE.getStatus()));
            Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
                    .put(user2.getId(), user2).build();
            when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
            // mock 移除发起人的用户
            springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class))
                    .thenReturn(processInstanceService);
            ProcessInstance processInstance = mock(ProcessInstance.class);
            when(processInstanceService.getProcessInstance(eq(processInstanceId))).thenReturn(processInstance);
            when(processInstance.getStartUserId()).thenReturn("1");
            mockFlowElementExtensionElement(userTask, BpmnModelConstants.USER_TASK_ASSIGN_START_USER_HANDLER_TYPE,
                    String.valueOf(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType()));

            // 调用
            Set<Long> results = taskCandidateInvoker.calculateUsersByTask(execution);
            // 断言
            assertEquals(asSet(2L), results);
        }
    }

    /**
     * 场景:没有计算到候选人,但是被禁用移除,最终通过 empty 进行分配
     */
    @Test
    public void testCalculateUsersByTask_none() {
        try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
            // 准备参数
            String param = "1,2";
            DelegateExecution execution = mock(DelegateExecution.class);
            // mock 方法(DelegateExecution)
            UserTask userTask = mock(UserTask.class);
            String processInstanceId = randomString();
            when(execution.getProcessInstanceId()).thenReturn(processInstanceId);
            when(execution.getCurrentFlowElement()).thenReturn(userTask);
            when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY)))
                    .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy().toString());
            when(userTask.getAttributeValue(eq(BpmnModelConstants.NAMESPACE), eq(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM)))
                    .thenReturn(param);
            // mock 方法(adminUserApi)
            AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
                    .setStatus(CommonStatusEnum.DISABLE.getStatus()));
            AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
                    .setStatus(CommonStatusEnum.DISABLE.getStatus()));
            Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
                    .put(user2.getId(), user2).build();
            when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
            // mock 方法(empty)
            when(emptyStrategy.calculateUsersByTask(same(execution), same(param)))
                    .thenReturn(Sets.newSet(2L));
            // mock 移除发起人的用户
            springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class))
                    .thenReturn(processInstanceService);
            ProcessInstance processInstance = mock(ProcessInstance.class);
            when(processInstanceService.getProcessInstance(eq(processInstanceId))).thenReturn(processInstance);
            when(processInstance.getStartUserId()).thenReturn("1");

            // 调用
            Set<Long> results = taskCandidateInvoker.calculateUsersByTask(execution);
            // 断言
            assertEquals(asSet(2L), results);
        }
    }

    /**
     * 场景:没有计算到候选人,但是被禁用移除,最终通过 empty 进行分配
     */
    @Test
    public void testCalculateUsersByActivity_some() {
        try (MockedStatic<BpmnModelUtils> bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) {
            // 准备参数
            String param = "1,2";
            BpmnModel bpmnModel = mock(BpmnModel.class);
            String activityId = randomString();
            Long startUserId = 1L;
            String processDefinitionId = randomString();
            Map<String, Object> processVariables = new HashMap<>();
            // mock 方法(DelegateExecution)
            UserTask userTask = mock(UserTask.class);
            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateStrategy(same(userTask)))
                    .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy());
            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateParam(same(userTask)))
                    .thenReturn(param);
            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))).thenReturn(userTask);
            // mock 方法(adminUserApi)
            AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
                    .setStatus(CommonStatusEnum.ENABLE.getStatus()));
            AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
                    .setStatus(CommonStatusEnum.ENABLE.getStatus()));
            Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
                    .put(user2.getId(), user2).build();
            when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
            // mock 移除发起人的用户
            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseAssignStartUserHandlerType(same(userTask)))
                    .thenReturn(BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType());

            // 调用
            Set<Long> results = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId,
                    startUserId, processDefinitionId, processVariables);
            // 断言
            assertEquals(asSet(2L), results);
        }
    }

    /**
     * 场景:成功计算到候选人,但是移除了发起人的用户
     */
    @Test
    public void testCalculateUsersByActivity_none() {
        try (MockedStatic<BpmnModelUtils> bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class)) {
            // 准备参数
            String param = "1,2";
            BpmnModel bpmnModel = mock(BpmnModel.class);
            String activityId = randomString();
            Long startUserId = 1L;
            String processDefinitionId = randomString();
            Map<String, Object> processVariables = new HashMap<>();
            // mock 方法(DelegateExecution)
            UserTask userTask = mock(UserTask.class);
            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateStrategy(same(userTask)))
                    .thenReturn(BpmTaskCandidateStrategyEnum.USER.getStrategy());
            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseCandidateParam(same(userTask)))
                    .thenReturn(param);
            bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.getFlowElementById(same(bpmnModel), eq(activityId))).thenReturn(userTask);
            // mock 方法(adminUserApi)
            AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
                    .setStatus(CommonStatusEnum.DISABLE.getStatus()));
            AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
                    .setStatus(CommonStatusEnum.DISABLE.getStatus()));
            Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
                    .put(user2.getId(), user2).build();
            when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
            // mock 方法(empty)
            when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId),
                            eq(param), same(startUserId), same(processDefinitionId), same(processVariables)))
                    .thenReturn(Sets.newSet(2L));

            // 调用
            Set<Long> results = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId,
                    startUserId, processDefinitionId, processVariables);
            // 断言
            assertEquals(asSet(2L), results);
        }
    }

    private static void mockFlowElementExtensionElement(FlowElement element, String name, String value) {
        if (value == null) {
            return;
        }
        ExtensionElement extensionElement = new ExtensionElement();
        extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
        extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX);
        extensionElement.setElementText(value);
        extensionElement.setName(name);
        // mock
        Map<String, List<ExtensionElement>> extensionElements = element.getExtensionElements();
        if (extensionElements == null) {
            extensionElements = new LinkedHashMap<>();
        }
        extensionElements.put(name, Collections.singletonList(extensionElement));
        when(element.getExtensionElements()).thenReturn(extensionElements);
    }

    @Test
    public void testRemoveDisableUsers() {
        // 准备参数. 1L 可以找到;2L 是禁用的;3L 找不到
        Set<Long> assigneeUserIds = asSet(1L, 2L, 3L);
        // mock 方法
        AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L)
                .setStatus(CommonStatusEnum.ENABLE.getStatus()));
        AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L)
                .setStatus(CommonStatusEnum.DISABLE.getStatus()));
        Map<Long, AdminUserRespDTO> userMap = MapUtil.builder(user1.getId(), user1)
                .put(user2.getId(), user2).build();
        when(adminUserApi.getUserMap(eq(assigneeUserIds))).thenReturn(userMap);

        // 调用
        taskCandidateInvoker.removeDisableUsers(assigneeUserIds);
        // 断言
        assertEquals(asSet(1L), assigneeUserIds);
    }

}