com.knowprocess.bpm.bdd.BpmSpec.java Source code

Java tutorial

Introduction

Here is the source code for com.knowprocess.bpm.bdd.BpmSpec.java

Source

/*******************************************************************************
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * BPM Behaviour Driven Development (BDD) library
 * Copyright 2015 Tim Stephenson
 *
 *******************************************************************************/
package com.knowprocess.bpm.bdd;

import static com.knowprocess.bpm.bdd.assertions.BpmAssert.assertProcessEnded;
import static com.knowprocess.bpm.bdd.assertions.BpmAssert.assertProcessEndedAndInEndEvents;
import static com.knowprocess.bpm.bdd.assertions.BpmAssert.assertProcessEndedAndInExclusiveEndEvent;
import static com.knowprocess.bpm.bdd.assertions.BpmAssert.assertProcessVariableLatestValueEquals;
import static com.knowprocess.bpm.bdd.assertions.BpmAssert.processEngine;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.Set;

import org.apache.commons.lang3.tuple.ImmutablePair;
import org.flowable.engine.common.api.FlowableObjectNotFoundException;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.impl.test.JobTestHelper;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.engine.test.FlowableRule;
import org.flowable.idm.api.User;
import org.flowable.job.api.Job;
import org.flowable.task.api.Task;

/**
 * Builds and runs process acceptance test cases using a fluent API.
 *
 * @author Tim Stephenson
 */
public class BpmSpec {

    private static final Set<String> emptySet = new HashSet<String>();

    private FlowableRule flowableRule;

    private String specName;

    private ProcessInstance processInstance;

    private String messageName;

    private HashMap<String, Object> collectVars;

    private String processDefinitionKey;

    public BpmSpec(FlowableRule flowableRule, String name) {
        this.flowableRule = flowableRule;
        this.specName = name;

        this.collectVars = new HashMap<String, Object>();
        processEngine = flowableRule.getProcessEngine();
        writeBddPhrase("Instantiated specification for scenario %1$s", specName);
    }

    /**
     * @param preCondition
     *            Natural language definition of scenario pre-conditions.
     * @return
     */
    public BpmSpec given(String preCondition) {
        writeBddPhrase("%1$sGIVEN: %2$s", System.getProperty("line.separator"), preCondition);
        return this;
    }

    /**
     * Write a BDD phrase (Given, When or Then ...).
     * 
     * <p>
     * Default implementation writes to System.out.
     * 
     * @param phrase
     */
    protected void writeBddPhrase(String phrase) {
        System.out.println(phrase);
    }

    /**
     * Write a BDD phrase (Given, When or Then ...).
     * 
     * <p>
     * Default implementation writes to System.out.
     * 
     * @param phrase
     * @param args
     *            Substitution arguments for phrase.
     */
    protected void writeBddPhrase(String format, Object... args) {
        writeBddPhrase(String.format(format, args));
    }

    public Object getVar(String varName) {
        return collectVars.get(varName);
    }

    /**
     * @return The process instance started by the specification.
     */
    public ProcessInstance getProcessInstance() {
        return processInstance;
    }

    public BpmSpec whenEventOccurs(String eventDescription, String key, Set<String> collectVars,
            Map<String, Object> putVars) {
        this.processDefinitionKey = key;

        HashMap<String, Object> vars = new HashMap<String, Object>();
        for (Entry<String, Object> entry : putVars.entrySet()) {
            vars.put(entry.getKey(), entry.getValue());
        }
        processInstance = flowableRule.getRuntimeService().startProcessInstanceByKey(processDefinitionKey, vars);
        assertNotNull(processInstance);
        assertNotNull(processInstance.getId());

        writeBddPhrase("WHEN: %1$s", eventDescription);
        return this;
    }

    /**
     * Define the start event for the business process.
     * 
     * @param eventDescription
     *            'When' phase of scenario.
     * @param key
     *            Specifies the Process Definition to start.
     * @param collectVars
     *            Process variables to collect in the specification.
     * @param putVars
     *            Process variables to inject at process start.
     * @param tenantId
     *            Process tenant, may be null.
     * @return The updated specification.
     */
    public BpmSpec whenEventOccurs(String eventDescription, String key, Set<String> collectVars,
            Map<String, Object> putVars, String tenantId) {
        this.processDefinitionKey = key;

        HashMap<String, Object> vars = new HashMap<String, Object>();
        for (Entry<String, Object> entry : putVars.entrySet()) {
            vars.put(entry.getKey(), entry.getValue());
        }
        processInstance = flowableRule.getRuntimeService()
                .startProcessInstanceByKeyAndTenantId(processDefinitionKey, vars, tenantId);
        assertNotNull(processInstance);
        assertNotNull(processInstance.getId());

        writeBddPhrase("WHEN: %1$s", eventDescription);
        return this;
    }

    /**
     * Define the message start event for the business process.
     * 
     * @param eventDescription
     *            'When' phase of scenario.
     * @param msgName
     *            Specifies the message name identifying the Process Definition
     *            to start.
     * @param messageResource
     *            Classpath resource to load and inject as process variable or
     *            the variable itself as a string.
     * @param tenantId
     *            Process tenant, may be null.
     * @return The updated specification.
     */
    public BpmSpec whenMsgReceived(String eventDescription, String msgName, String messageResource,
            String tenantId) {
        this.messageName = msgName;

        HashMap<String, Object> vars = new HashMap<String, Object>();
        vars.put("messageName", adapt(msgName));
        vars.put(adapt(messageName), getJson(messageResource));

        processInstance = flowableRule.getRuntimeService().startProcessInstanceByMessageAndTenantId(msgName, vars,
                tenantId);
        assertNotNull(processInstance);
        assertNotNull(processInstance.getId());

        writeBddPhrase("WHEN: %1$s", eventDescription);
        return this;
    }

    /**
     * Define the message to send to be caught by an intermediate event of the
     * business process.
     * 
     * @param eventDescription
     *            'When' phase of scenario.
     * @param msgName
     *            Specifies the message name identifying the Process Definition
     *            to interact with.
     * @param messageResource
     *            Classpath resource to load and inject as process variable or
     *            the variable itself as a string.
     * @param tenantId
     *            Process tenant, may be null.
     * @return The updated specification.
     */
    public BpmSpec whenFollowUpMsgReceived(String eventDescription, String msgName, String messageResource,
            String tenantId) {
        this.messageName = msgName;

        HashMap<String, Object> vars = new HashMap<String, Object>();
        vars.put("messageName", adapt(msgName));
        vars.put(adapt(messageName), getJson(messageResource));

        //        flowableRule.getRuntimeService().signal(processInstance.getId(), vars);

        writeBddPhrase("WHEN: %1$s", eventDescription);
        return this;
    }

    protected String getJson(String messageResource) {
        InputStream is = null;
        Reader source = null;
        Scanner scanner = null;
        String json = null;
        try {
            is = getClass().getResourceAsStream(messageResource);
            // assertNotNull("Unable to load test resource: " + messageResource,
            // is);
            source = new InputStreamReader(is);
            scanner = new Scanner(source);
            json = scanner.useDelimiter("\\A").next();
        } catch (NullPointerException e) {
            // assume message supplied directly
            json = messageResource;
        } finally {
            try {
                scanner.close();
            } catch (Exception e) {
                ;
            }
        }
        return json;
    }

    /**
     * Script Task resulting from the scenario specification.
     * 
     * <p>
     * Task will be asserted to have been completed, variables collected and/or
     * updated and then completed.
     * 
     * @param taskDefinitionKey
     *            Key (BPMN id) for task.
     * @param collectVars
     *            Variable names to collect in the scenario.
     * @return The updated specification.
     */
    public BpmSpec thenScriptTask(String taskDefinitionKey, Set<String> collectVars) {
        return thenServiceTask(taskDefinitionKey, collectVars);
    }

    public BpmSpec thenScriptTask(String taskDefinitionKey) {
        return thenServiceTask(taskDefinitionKey, emptySet);
    }

    /**
     * Service Task resulting from the scenario specification.
     * 
     * <p>
     * Task will be asserted to have been completed, variables collected and/or
     * updated and then completed.
     * 
     * @param taskDefinitionKey
     *            Key (BPMN id) for service task.
     * @param collectVars
     *            Variable names to collect in the scenario.
     * @return The updated specification.
     */
    public BpmSpec thenServiceTask(String taskDefinitionKey, Set<String> collectVars) {
        List<HistoricActivityInstance> tasks = flowableRule.getHistoryService()
                .createHistoricActivityInstanceQuery().activityId(taskDefinitionKey).list();

        assertTrue("Did not find the expected task with id " + taskDefinitionKey, tasks.size() != 0);

        for (String varName : collectVars) {
            collectVar(varName);
        }

        writeBddPhrase("THEN: Task '%1$s' was created and completed", taskDefinitionKey);
        return this;
    }

    public BpmSpec thenServiceTask(String taskDefinitionKey) {
        return thenServiceTask(taskDefinitionKey, emptySet);
    }

    /**
     * User Task resulting from the scenario specification.
     * 
     * <p>
     * Task will be asserted to exist, variables collected and/or updated and
     * then completed.
     * 
     * @param taskDefinitionKey
     *            Key (BPMN id) for user task.
     * @param collectVars
     *            Variable names to collect in the scenario.
     * @param putVars
     *            Variables to be injected into the process context.
     * @return The updated specification.
     */
    public BpmSpec thenUserTask(String taskDefinitionKey, Set<String> collectVars, Map<String, Object> putVars) {
        Task task = flowableRule.getTaskService().createTaskQuery().singleResult();
        assertNotNull("Did not find the expected task with key " + taskDefinitionKey, task);
        assertEquals(taskDefinitionKey, task.getTaskDefinitionKey());

        for (String varName : collectVars) {
            collectVar(varName);
        }

        HashMap<String, Object> vars = new HashMap<String, Object>();
        for (Entry<String, Object> entry : putVars.entrySet()) {
            vars.put(entry.getKey(), entry.getValue());
        }

        flowableRule.getTaskService().complete(task.getId(), vars, false);
        for (Entry<String, Object> entry : putVars.entrySet()) {
            assertProcessVariableLatestValueEquals(processInstance, entry.getKey(), entry.getValue());
        }
        writeBddPhrase("THEN: User Task '%1$s' is created and completed", taskDefinitionKey);
        return this;
    }

    /**
     * Execute an extension action for the scenario.
     * 
     * <p>
     * For example may be used to change state the process will subsequently
     * need or to perform custom assertions about the scenario.
     * 
     * @param action
     * @return The updated specification.
     * @throws Exception
     */
    public BpmSpec thenExtension(ExternalAction action) throws Exception {
        action.execute(this);
        writeBddPhrase("THEN: extension '%1$s' is run", action.getClass().getName());
        return this;
    }

    /**
     * A scenario event allowing the process engine to execute for the specified
     * period.
     * 
     * @param maxMillisToWait
     *            Maximum milli-seconds to allow the engine to execute.
     * @return The updated specification.
     */
    public BpmSpec whenExecuteJobsForTime(int maxMillisToWait) {
        JobTestHelper.executeJobExecutorForTime(flowableRule, maxMillisToWait, 1);

        List<Job> jobs = flowableRule.getManagementService().createJobQuery().list();
        writeBddPhrase("WHEN: executed jobs for %1$d, %2$d jobs remained", maxMillisToWait, jobs.size());

        return this;
    }

    /**
     * A scenario event allowing the process engine to execute for the specified
     * period.
     * 
     * @param timeout
     *            Maximum milli-seconds to allow the engine to execute.
     * @return The updated specification.
     */
    public BpmSpec whenExecuteAllJobs(int timeout) {
        JobTestHelper.waitForJobExecutorToProcessAllJobs(
                flowableRule.getProcessEngine().getProcessEngineConfiguration(),
                flowableRule.getManagementService(), timeout, 1);
        writeBddPhrase("WHEN: executed all jobs");
        return this;
    }

    /**
     * Advances process engine by the specified amount of time.
     * 
     * @param field
     *            One of the field constants in java.util.Calendar.
     * @param amount
     *            Amount to change field by.
     * @return The updated specification.
     * @see <a
     *      href="https://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html">java.util.Calendar</a>
     */
    public BpmSpec whenProcessTimePassed(int field, int amount) {
        Calendar cal = flowableRule.getProcessEngine().getProcessEngineConfiguration().getClock()
                .getCurrentCalendar();
        cal.add(field, amount);
        Date time = cal.getTime();
        writeBddPhrase("WHEN: process time advanced to : %1$s", time.toString());
        flowableRule.setCurrentTime(time);
        return this;
    }

    /**
     * Assert that the specified sub-process callActivity has actually been
     * invoked.
     * 
     * @param subProcDefKey
     *            Key (id without vsn info) of the sub-process that should have
     *            been invoked.
     * @return The updated specification.
     */
    public BpmSpec thenSubProcessCalled(String subProcDefKey) {
        if (subProcDefKey == null) {
            throw new IllegalArgumentException("Parameter subProcId must not be null");
        }

        boolean found = searchForSubProc(subProcDefKey, processInstance.getId());

        assertTrue(String.format("No call made to %1$s", subProcDefKey), found);
        writeBddPhrase("THEN: The sub-process %1$s is called", subProcDefKey);
        return this;
    }

    private boolean searchForSubProc(String subProcDefKey, String procId) {
        List<HistoricProcessInstance> childProcessInstances = flowableRule.getHistoryService()
                .createHistoricProcessInstanceQuery().superProcessInstanceId(procId).list();

        for (HistoricProcessInstance hpi : childProcessInstances) {
            if (hpi.getProcessDefinitionId().startsWith(subProcDefKey)) {
                return true;
            }
        }
        for (HistoricProcessInstance hpi : childProcessInstances) {
            if (searchForSubProc(subProcDefKey, hpi.getId())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Verify that the outcome of the scenario is that the process is complete.
     * 
     * @return The updated specification.
     */
    public BpmSpec thenProcessIsComplete() {
        assertProcessEnded(processInstance);
        writeBddPhrase("THEN: The process is complete");
        return this;
    }

    /**
     * Verify that the outcome of the scenario is that the process completed in
     * the all the BPMN event ids.
     * 
     * @param endEventId
     * @return The updated specification.
     */
    public BpmSpec thenProcessEndedAndInEndEvents(String... endEventIds) {
        assertProcessEndedAndInEndEvents(processInstance, endEventIds);
        writeBddPhrase("THEN: The process is complete and finished in these events %1$s", (Object[]) endEventIds);
        return this;
    }

    /**
     * Verify that the outcome of the scenario is that the process completed in
     * the one and only BPMN event id.
     * 
     * @param endEventId
     * @return The updated specification.
     */
    public BpmSpec thenProcessEndedAndInExclusiveEndEvent(String endEventId) {
        // ProcessInstance processInstance2 = flowableRule.getProcessEngine()
        // .getRuntimeService().createProcessInstanceQuery()
        // .processInstanceId(processInstance.getId()).singleResult();
        HistoricProcessInstance processInstance2 = flowableRule.getProcessEngine().getHistoryService()
                .createHistoricProcessInstanceQuery().processInstanceId(processInstance.getId()).singleResult();
        assertNotNull(processInstance2);
        assertNotNull(processInstance2.getEndTime());

        assertProcessEndedAndInExclusiveEndEvent(processInstance, endEventId);
        writeBddPhrase("THEN: The process is complete and in the end event %1$s", endEventId);
        return this;
    }

    public BpmSpec thenTimerExpired(String timerEventId) {
        List<HistoricActivityInstance> flowablees = flowableRule.getHistoryService()
                .createHistoricActivityInstanceQuery().activityId(timerEventId).list();
        assertEquals(1, flowablees.size());
        writeBddPhrase("THEN: The timer %1$s expired", timerEventId);
        return this;
    }

    public BpmSpec thenUserExists(String userId, String... groupIds) {
        User user = flowableRule.getIdentityService().createUserQuery().userId(userId).singleResult();
        assertNotNull(user);

        for (String groupId : groupIds) {
            assertTrue(flowableRule.getIdentityService().createGroupQuery().groupId(groupId).count() > 0);
        }
        writeBddPhrase("THEN: The user %1$s exists", userId);
        return this;
    }

    public BpmSpec thenUserAuthenticated(String userId, String newPwd) {
        flowableRule.getIdentityService().checkPassword(userId, newPwd);
        return this;
    }

    /**
     * 
     * @param varName
     * @return The updated specification.
     */
    public BpmSpec collectVar(String varName) {
        Object var = null;
        try {
            var = flowableRule.getRuntimeService().getVariable(processInstance.getId(), varName);
        } catch (FlowableObjectNotFoundException e) {
            // assume process ended, try history
            var = flowableRule.getHistoryService().createHistoricVariableInstanceQuery()
                    .processInstanceId(processInstance.getId()).variableName(varName).singleResult().getValue();
        }
        System.out.println(String.format("%1$s: %2$s", varName, var));
        assertNotNull(var);
        collectVars.put(varName, var);
        return this;
    }

    private String adapt(String msgName) {
        return msgName.replace('.', '_');
    }

    /**
     * Creates an immutable pair to specify a process variable.
     * 
     * @param varName
     * @param varValue
     * @return an immutable pair representing a scenario variable.
     */
    public static ImmutablePair<String, Object> newPair(String varName, Object varValue) {
        return new ImmutablePair<String, Object>(varName, varValue);
    }

    /**
     * Convenience method to create a set of strings.
     * 
     * @param strings
     * @return
     */
    public static Set<String> buildSet(String... strings) {
        Set<String> set = new HashSet<String>();
        for (String s : strings) {
            set.add(s);
        }
        return set;
    }

    /**
     * Convenience method to create variable map.
     * 
     * @param immutablePair
     * @return
     */
    public static Map<String, Object> buildMap(ImmutablePair<String, Object>... immutablePair) {
        Map<String, Object> map = new HashMap<String, Object>();
        for (ImmutablePair<String, Object> pair : immutablePair) {
            map.put(pair.getKey(), pair.getValue());
        }
        return map;
    }

    public static Set<String> emptySet() {
        return emptySet;
    }

}