Whatever your favorite framework for testing may be, JUnit or TestNG or anything similar, you should not have a problem unit testing your individual pieces of code that are invoked throughout your business process. That means that whatever you're using as 'applications' in a Stardust process, it can most likely be unit tested. This could be a Pojo that performs some calculations, a business rule, or a web service. Even UIs can be unit tested given the appropriate tools, e.g. Selenium.
This article discusses the usage of a testing framework to test the 'big picture' once all the above mentioned pieces have been integrated into an end-to-end business process.
Testing business processes
First of all the question arises how to perform tests against a complete Stardust runtime environment. Test suites generally should be self-contained: they should be able to set up their own environment reproducibly and clean up after themselves. But that is not always possible. In case you're running Stardust in EJB mode on a J2EE application server, the effort to control the environment through your test cases might be too great and when running tests, the answer might simply be to manually set up an environment for the tests by making sure that the server is started up and the Stardust runtime is deployed and configured correctly. With Stardust deployed in Spring mode it becomes easier to create your Stardust runtime environment specifically for the test runs. The downloadable Stardust Process Driver project contains this JUnit4 test case which demonstrates this approach. It performs the following steps:
- The global startup phase of the test is executed only once and it takes care of the following tasks:
- It checks for a Derby database instance to be used and it creates one automatically if needed.
- After booting the database it tries to query a table from the Stardust AuditTrail schema. If unsuccessful, the AuditTrail schema is created.
- It then boots a Spring-based Stardust runtime environment.
- Finally a process model is deployed into this environment.
- Then, all individual tests are executed.
Afterwards, the process model which was deployed during the startup is removed which results in all created processes being deleted as well, effectively leaving no traces of the test runs.
For the individual tests the question is 'What assumptions about your processes do you want to test'? Here are some typical examples:
If I run a XYZ process with an amount greater than $100,000 dollars, I need it to go through the review step.
When the initial automatic validation checks and message parsing of my process are completed, I expect to find a 'Select Contract' work item in the workbasket of role 'ABC'
After completing the AddressChange process, the client record in my database should be up-to-date with the new address.
In order to automate this testing, you sometimes need to 'drive' the process forward, e.g. to simulate a user completing a work item. Furthermore, the state of a process and its data needs to be queried to verify test assumptions. All of this can be accomplished using the Stardust API.
Completing an activity
When a process instance arrives at an activity that does not complete automatically, e.g. a manual activity, you can use the WorkflowService to simulate the user's interaction. The Map argument that is passed into the following method contains the IDs and data of the OUT-Data-Mappings for that activity as specified in the process model to pass data back to the process.
workflowService.activateAndComplete(long activityInstanceOID, String context, Map outData)
Manipulating process data
The following API calls can be used to read and write process data. The data you want to access this way must be declared as IN- or OUT-Data-Path in the properties of the process definition in the process model with IN corresponding to read access and OUT specifying write privileges.
workflowService.getInDataPath(long processInstanceOID, String id) workflowService.getInDataPaths(long processInstanceOID, Set ids) workflowService.setOutDataPath(long processInstanceOID, String id, Object object) workflowService.setOutDataPaths(long processInstanceOID, Map values)
Querying process and activity state
The Stardust Query Framework can be used to retrieve process instances as well as activity instances while applying a wide range of filter criteria. Following are merely a few examples. For the full depth of the Query Framework's functionalities please refer to the Stardust documentation.
ProcessInstanceQuery piQuery = ProcessInstanceQuery.findForProcess("AccountOpening"); piQuery.where( DataFilter.isEqual("Customer", "lastname", "Smith"); int resultCount = queryService.getProcessInstancesCount( piQuery );
The above code snippet queries for the number of instances of the Account Opening process for a customer with lastname 'Smith'. Here's another example using an ActivityInstanceQuery.
ActivityInstanceQuery aiQuery = ActivityInstanceQuery.findInState( ActivityInstanceState.Hibernated ); aiQuery.where( ActivityFilter.forProcess("AccountOpening", "WaitingForSignedContract") ); Calendar cal = Calendar.getInstance(); cal.add( Calendar.DAY_OF_MONTH, -1 ); cal.set( Calendar.HOUR_OF_DAY, 8 ); aiQuery.where( ActivityInstanceQuery.START_TIME.lessThan(cal.getTimeInMillis()) ); ActivityInstances instances = queryService.getAllActivityInstances( aiQuery );
In this example the query would retrieve all instances of the WaitingForSignedContract activity belonging to the Account Opening process which are currently in a hibernated state. An additional filter is applied to find only those instances that were created before 8:00 AM yesterday. This kind of query could be interesting in a test scenario where you would not expect to find any instances, because an escalation timer had been created for this activity in the process model. This query returning any instances would then indicate that the escalation did not work.
Every business process is unique in a way and even though testing all of its aspects could be automated, the efforts needed to do so vary greatly and in practice it's not always worth it. Therefore, how to use the approaches and API calls presented in this article needs to be discussed differently from project to project. A lot of times it may even be cheaper to rely on a larger number of people to cover testing the fully integrated system than to invest into writing automated test cases.
Stardust Process Driver
The Stardust Process Driver is a project that is intended to provide tools and shortcuts for the tasks described in the section above. It was first created to be able to (re-) create a certain state in the AuditTrail database. In its current version it supports creating a configurable number of processes while manipulating their start time and the timestamps when their activities get completed. It therefore serves the purpose of a state similar to a 'daily snapshot' of an environment that has been used for quite some time. There are two approaches to feed the process driver with instructions. The first uses a combination of properties file and Spring while the second one uses a pure Spring-based configuration.
- Use Spring to define named sets of data which can be reused when starting processes. The example shows how to create a structured data type object (SDT) called 'Customer'. This can be passed into a starting process to fill some of its data objects.
<bean id="processData_Set1" class="com.infinity.bpm.util.KeyValueList"> <property name="keyValueList"> <list> <value>Customer#id;12345;long</value> <value>Customer#firstname;John;</value> <value>Customer#lastname;Smith;</value> </list> </property> </bean> <bean id="processData_Set2" class="com.infinity.bpm.util.KeyValueList"> <property name="keyValueList"> <list> <value>Customer#id;54321;long</value> <value>Customer#firstname;Jane;</value> <value>Customer#lastname;Doe;</value> </list> </property> </bean>
- Specify instructions about the number of processes and completed activities either in Spring or in a properties file. The following example accomplishes the following:
- Create 5 instances of the TestProcess which start on 02-May-2009 at 8 am.
- Complete the first two activities of this process.
- Wait 480 seconds between activating and completing the activity (simulated user's time spent working on the task).
- Wait 180 seconds before creating and activating a new activity (artificially adding age to the process).
- Use the data set with the name 'DataSet1' (in Spring a ref is used) when starting the processes.
- 1. As a line in a properties file:
- TestProcess, 5, 02.05.2009 08:00:00, 2, 480, 180, DataSet1
- 2. Inside the Spring context:
<bean id="driverInstructions1" class="com.infinity.bpm.tools.processdriver.ProcessDriverInstructionSet" > <property name="processId" value="TestProcess" /> <property name="numberOfProcesses" value="5" /> <property name="startTimeAsString" value="02.05.2009 08:00:00" /> <property name="completedActivities" value="2" /> <property name="activityCompletionDelay" value="480" /> <property name="activityActivationDelay" value="180" /> <property name="processData" ref="processData_Set1" /> </bean>
The artifacts referenced in this article may be downloaded from here. This includes the Java source code, JUnit testcase and the above example configuration as well as a matching process model.