This blog is a continuation of my first blog around the work we Ciju Joseph and Francesco Corti did as part of Alfresco Global Virtual Hack-a-thon 2017
In this blog I’ll be walking you through aps-unit-test-example project we created where I’ll be using the features from the aps-unit-test-utils library which I explained in the first blog.
This project contains a lot of examples showing:
Before even we get to the unit testing part, it is very important to understand the project structure.
As you can see from the above diagram, this is a maven project. However, if you are a “gradle” person, you should be able to do it the gradle way too! The various sections of the project are:
<dependency>
<groupId>com.alfresco.aps</groupId>
<artifactId>aps-unit-test-utils</artifactId>
<version>[1.0-SNAPSHOT,)</version>
</dependency>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<configuration>
<descriptors>
<descriptor>src/main/resources/assembly/assembly.xml</descriptor>
</descriptors>
</configuration>
<id>create-distribution</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
Now that you have a good understanding of all the project components, let’s take a look at some of the examples available in the project. I have tried my very best to keep the test classes and processes as simple as possible to make it easy for everyone to follow without much explanation.
AbstractBpmnTest.java - This class can be used as a parent class for all the BPMN test classes. To avoid writing the same logic in multiple test classes, I added a few common logic into this, they are:
/* Including it in the Abstract Class to avoid writing this in all the Tests.
* Pre-test logic flow -
* 1) Download from APS if system property -Daps.app.download=true
* 2) Find all the bpmn20.xml's in {@value
* BPMN_RESOURCE_PATH} and deploy to process engine
* 3) Find all the elements in the process that is being tested. This set will
* be compared with another set that contains the process elements that are
* covered in each tests (this get updated after each tests).
*/
@Before
public void before() throws Exception {
if (System.getProperty("aps.app.download") != null && System.getProperty("aps.app.download").equals("true")) {
ActivitiResources.forceGet(appName);
}
Iterator<File> it = FileUtils.iterateFiles(new File(BPMN_RESOURCE_PATH), null, false);
while (it.hasNext()) {
String bpmnXml = ((File) it.next()).getPath();
String extension = FilenameUtils.getExtension(bpmnXml);
if (extension.equals("xml")) {
repositoryService.createDeployment().addInputStream(bpmnXml, new FileInputStream(bpmnXml)).deploy();
}
}
processDefinitionId = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey(processDefinitionKey).singleResult().getId();
List<Process> processList = repositoryService.getBpmnModel(processDefinitionId).getProcesses();
for (Process proc : processList) {
for (FlowElement flowElement : proc.getFlowElements()) {
if (!(flowElement instanceof SequenceFlow)) {
flowElementIdSet.add(flowElement.getId());
}
}
}
}
/*
* Post-test logic flow -
* 1) Update activityIdSet (Set containing all the elements tested)
* 2) Delete all deployments
*/
@After
public void after() {
for (HistoricActivityInstance act : historyService.createHistoricActivityInstanceQuery().list()) {
activityIdSet.add(act.getActivityId());
}
List<Deployment> deploymentList = activitiRule.getRepositoryService().createDeploymentQuery().list();
for (Deployment deployment : deploymentList) {
activitiRule.getRepositoryService().deleteDeployment(deployment.getId(), true);
}
}
/*
* Tear down logic - Compare the flowElementIdSet with activityIdSet and
* alert the developer if some parts are not tested
*/
@AfterClass
public static void afterClass() {
if (!flowElementIdSet.equals(activityIdSet)) {
System.out.println(
"***********PROCESS TEST COVERAGE WARNING: Not all paths are being tested, please review the test cases!***********");
System.out.println("Steps In Model: " + flowElementIdSet);
System.out.println("Steps Tested: " + activityIdSet);
}
}
In this example we will test the following process diagram which is a simple process containing three steps Start → User Task → End
UserTaskUnitTest.java - test class associated with this process which tests the following
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:activiti.cfg.xml", "classpath:common-beans-and-mocks.xml" })
public class UserTaskUnitTest extends AbstractBpmnTest {
/*
* Setting the App name to be downloaded if run with -Daps.app.download=true
* Also set the process definition key of the process that is being tested
*/
static {
appName = "Test App";
processDefinitionKey = "UserTaskProcess";
}
@Test
public void testProcessExecution() throws Exception {
/*
* Creating a map and setting a variable called "initiator" when
* starting the process.
*/
Map<String, Object> processVars = new HashMap<String, Object>();
processVars.put("initiator", "$INITIATOR");
/*
* Starting the process using processDefinitionKey and process variables
*/
ProcessInstance processInstance = activitiRule.getRuntimeService()
.startProcessInstanceByKey(processDefinitionKey, processVars);
/*
* Once started assert that the process instance is not null and
* successfully started
*/
assertNotNull(processInstance);
/*
* Since the next step after start is a user task, doing a query to find
* the user task count in the engine. Assert that it is only 1
*/
assertEquals(1, taskService.createTaskQuery().count());
/*
* Get the Task object for further task assertions
*/
Task task = taskService.createTaskQuery().singleResult();
/*
* Asserting the task for things such as assignee, due date etc. Also,
* at the end of it complete the task Using the custom assertion
* TaskAssert from the utils project here
*/
TaskAssert.assertThat(task).hasAssignee("$INITIATOR", false, false).hasDueDate(2, TIME_UNIT_DAY).complete();
/*
* Using the custom assertion ProcessInstanceAssert, make sure that the
* process is now ended.
*/
ProcessInstanceAssert.assertThat(processInstance).isComplete();
}
}
Let’s now look at a process that is a little more complex than the previous one. As you can see from the diagrams below, there are two units that are candidates for unit test in this model, they are process model & DMN model
AbstractDmnTest.java - Similar to the AbstractBpmnTest class I explained above, this class can be used as a parent class for all the DMN test classes. To avoid writing the same logic in multiple test classes, I added a few common logic into this, they are:
/*
* Including it in the Abstract Class to avoid writing this in all the
* Tests. Pre test logic -
* 1) Download from APS if system property -Daps.app.download=true
* 2) Find all the dmn files in {@value
* DMN_RESOURCE_PATH} and deploy to dmn engine
*/
@Before
public void before() throws Exception {
if (System.getProperty("aps.app.download") != null && System.getProperty("aps.app.download").equals("true")) {
ActivitiResources.forceGet(appName);
}
// Deploy the dmn files
Iterator<File> it = FileUtils.iterateFiles(new File(DMN_RESOURCE_PATH), null, false);
while (it.hasNext()) {
String bpmnXml = ((File) it.next()).getPath();
String extension = FilenameUtils.getExtension(bpmnXml);
if (extension.equals("dmn")) {
DmnDeployment dmnDeplyment = repositoryService.createDeployment()
.addInputStream(bpmnXml, new FileInputStream(bpmnXml)).deploy();
deploymentList.add(dmnDeplyment.getId());
}
}
}
/*
* Post test logic -
* 1) Delete all deployments
*/
@After
public void after() {
for (Long deploymentId : deploymentList) {
repositoryService.deleteDeployment(deploymentId);
}
deploymentList.clear();
}
In this example we will test the following DMN model which is a very simple decision table containing three rows of rules.
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:activiti.dmn.cfg.xml" })
public class DmnUnitTest extends AbstractDmnTest {
static {
appName = "Test App";
decisonTableKey = "dmntest";
}
/*
* Test a successful hit using all possible inputs
*/
@Test
public void testDMNExecution() throws Exception {
/*
* Invoke with input set to xyz and assert output is equal to abc
*/
Map<String, Object> processVariablesInput = new HashMap<>();
processVariablesInput.put("input", "xyz");
RuleEngineExecutionResult result = ruleService.executeDecisionByKey(decisonTableKey, processVariablesInput);
Assert.assertNotNull(result);
Assert.assertEquals(1, result.getResultVariables().size());
Assert.assertSame(result.getResultVariables().get("output").getClass(), String.class);
Assert.assertEquals(result.getResultVariables().get("output"), "abc");
/*
* Invoke with input set to 123 and assert output is equal to abc
*/
processVariablesInput.put("input", "123");
result = ruleService.executeDecisionByKey(decisonTableKey, processVariablesInput);
Assert.assertNotNull(result);
Assert.assertEquals(1, result.getResultVariables().size());
Assert.assertSame(result.getResultVariables().get("output").getClass(), String.class);
Assert.assertEquals(result.getResultVariables().get("output"), "abc");
/*
* Invoke with input set to abc and assert output is equal to abc
*/
processVariablesInput.put("input", "abc");
result = ruleService.executeDecisionByKey(decisonTableKey, processVariablesInput);
Assert.assertNotNull(result);
Assert.assertEquals(1, result.getResultVariables().size());
Assert.assertSame(result.getResultVariables().get("output").getClass(), String.class);
Assert.assertEquals(result.getResultVariables().get("output"), "abc");
}
/*
* Test a miss
*/
@Test
public void testDMNExecutionNoMatch() throws Exception {
Map<String, Object> processVariablesInput = new HashMap<>();
processVariablesInput.put("input", "dfdsf");
RuleEngineExecutionResult result = ruleService.executeDecisionByKey(decisonTableKey, processVariablesInput);
Assert.assertEquals(0, result.getResultVariables().size());
}
}
This section is about the testing of classes that you may write to support your process models. This includes testing of Java Delegates, Task Listeners, Event Listeners, Custom Rest Endpoints, Custom Extensions etc which are available under src/main/java. The naming convention I followed for the test classes is “<ClassName>Test.java” and the package name is the same package name of the class that we are testing.
Let’s now inspect an example which is the testing of a task listener named TaskAssignedTaskListener.java
The above task listener is used in a process named CustomListeners in the project. From a process testing perspective, this TaskListener is mocked in the process test class CustomListenersUnitTest.java via process-beans-and-mocks.xml. We now have this task listener class that is still not unit tested. Let’s inspect its testing class TaskAssignedTaskListenerTest.java which is tested the following way:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class TaskAssignedTaskListenerTest {
@Configuration
static class ContextConfiguration {
@Bean
public TaskAssignedTaskListener taskAssignedTaskListener() {
return new TaskAssignedTaskListener();
}
}
@InjectMocks
@Spy
private static TaskAssignedTaskListener taskAssignedTaskListener;
@Mock
private DelegateTask task;
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
/*
* Testing TaskAssignedTaskListener.notify(DelegateTask task) method using a
* mock DelegateTask created using Mockito library
*/
@Test
public void test() throws Exception {
/*
* Creating a map which will be used during the
* DelegateTask.getVariable() & DelegateTask.setVariable() calls from
* TaskAssignedTaskListener as well as from this test
*/
Map<String, Object> variableMap = new HashMap<String, Object>();
/*
* Stub a DelegateTask.setVariable() call
*/
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Object[] arg = invocation.getArguments();
variableMap.put((String) arg[0], arg[1]);
return null;
}
}).when(task).setVariable(anyString(), any());
/*
* Stub a DelegateTask.getVariable() call
*/
when(task.getVariable(anyString())).thenAnswer(new Answer<String>() {
public String answer(InvocationOnMock invocation) {
return (String) variableMap.get(invocation.getArguments()[0]);
}
});
/*
* Start the test by invoking the method on task listener
*/
taskAssignedTaskListener.notify(task);
/*
* sample assertion to make sure that the java code is setting correct
* value
*/
assertThat(task.getVariable("oddOrEven")).isNotNull().isIn("ODDDATE", "EVENDATE");
}
}
Checkout the whole project on GitHub where we have created a lot of examples that covers the unit testing of various types of BPMN components and scenarios. We’ll be adding more to this over the long run.
Hopefully this blog along with the other two unit-testing-part-1 & aps-ci-cd-example is of some help in the Lifecycle Management of Applications built using Alfresco Process Services powered by Activiti