cjose

Alfresco Process Services - Unit Testing # II

Blog Post created by cjose Employee on Oct 13, 2017

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.

About the Project

This project contains a lot of examples showing:

  • how to test various components in a  BPMN (Business Process Model and Notation) model
  • how to test a DMN (Decision Model and Notation) model
  • how to test custom java classes that are supporting your process models.

Project Structure

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:

  1. Main java classes - located under src/main/java. This includes all the custom java code that are supporting your process/dmn models.
  2. Test classes -  located under src/test/java. The tests are again grouped into different packages depending on the type of units they are.
    1. Java class tests - This includes test classes for classes (eg: Java Delegate, Task Listener, Event Listener, Custom Rest Endpoints, Custom Extensions etc) under src/main/java.
    2. DMN tests - As you can see from the package name (com.alfresco.aps.test.dmn) itself, I’m writing all the DMN tests under this package. The pattern I followed in this example is one test class per DMN file under the directory src/main/resources/app/decision-table-models.
    3. Process(BPMN) tests - Similar to DMN tests, the package com.alfresco.aps.test.process contains all the BPMN test classes. Similar to DMN tests, I am following the pattern of one test class per BPMN file under src/main/resources/app/bpmn-models
  3. App models - All the models (forms, bpmn, dmn, data models, stencils, app.json etc) that are part of the process application is stored under the directory src/main/resources/app. When using the aps-unit-test-utils which I explained in the previous article, all the models are downloaded to this directory from APS. Once the tests are passed successfully, we will re-build the deployable process artifacts from this directory
  4. Test resources - As with any standard java projects, you can keep all your test resources in the directory src/test/resources. I’ll highlight a couple of files that you will find under this directory in the above project structure image
    1. activiti-resources.properties - This file contains the APS server configurations such as server address, api url, user credentials etc for downloading the process application into your maven project. Please refer to my previous article for a detailed explanation of this file. You wouldn’t find this file on GitHub under this project, the reason is, this file is intended to be developer specific and local to the workspace of a developer. For this reason this file is included in the project’s .gitignore file to prevent it from getting saved to GitHub.
    2. process-beans-and-mocks.xml - the purpose of this file is to mock any project/process specific classes when you run your process tests. The concept is explained in detail in my previous article when I explained a similar file called common-beans-and-mocks.xml.  
  5. Build output - In the above screenshot you can see that there are two files named aps-unit-test-example-1.0-SNAPSHOT-App.zip and aps-unit-test-example-1.0-SNAPSHOT.jar under /target directory. This is basically the build output that gets generated when you package the app using maven commands such as “mvn clean package”. The “.zip” file is the app package created from src/main/resources/app directory which you can version after every build and deploy to higher environments. The “.jar” is the standard jar output including all the classes/resources from your src/main directory.
  6. Maven pom xml - Since this is a maven based project, you need a pom.xml under the root of the project. Highlighting some of the dependencies and plugins that are used in this pom.xml
    • aps-unit-test-utils dependency - the test utils project which I explained in my previous post/blog.
      <dependency>
           <groupId>com.alfresco.aps</groupId>
           <artifactId>aps-unit-test-utils</artifactId>
           <version>[1.0-SNAPSHOT,)</version>
      </dependency>
    • maven-compiler-plugin - a maven plugin that helps compile the sources of the project
      <plugin>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.6.2</version>
           <configuration>
                <source>1.8</source>
                <target>1.8</target>
           </configuration>
      </plugin>
    • maven-assembly-plugin - a maven plugin that is used to package the “app.zip” archive from src/main/resources/app
      <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>

Unit Test Examples

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.

Process Testing

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:

  • Setup of a mock email server
  • Process deployment prior to tests
  • Clean up such as delete all deployments after each tests
  • Test coverage alerts
/* 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);
          }
     }

Process Example 1

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

  • A process is started correctly
  • Upon start a user task is created and assigned to the correct user with the correct task due date
  • Upon completion of the user task the process is ended successfully
@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();
     }

}

Process Example 2

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

  • DMNProcessUnitTest.java - Similar to the above example, this is the test class associated with this process which tests the following:
    • A process is started correctly
    • Tests all possible paths in the process based on the output of rule step
    • Successful completion of process
    • Mocks the rules step - when it comes to the rules/decision step in the process, we are not invoking the actual DMN file associated with the process. From a process perspective all we care is that an appropriate variable is set at this step for it to take the respective path that is being tested. Hence the mock.
  • DmnUnitTest.java - This is the test class associated with the DMN file that is invoked from this process. More explanation in next section.

DMN Testing

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:

  • DMN deployment prior to tests
  • Clean up such as delete all deployments after each tests
/*
      * 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();
     }

DMN Example 1

In this example we will test the following DMN model which is a very simple decision table containing three rows of rules.

  • DmnUnitTest.java - the test class associated with the above DMN model. The test cases in this file will test every row in the DMN table and verify that it is getting executed as expected. The number of rules in real life can grow in size over time, hence it is important to have test cases covering all the possible hit and miss scenarios in your test cases for a healthy maintenance of your decision management and business 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());
     }

}

Custom Java Class Testing

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

Example 1

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:

  1. Set up mocks and inject mocks into classes that are being tested
  2. Set up mock answering stubs prior to execution
  3. Execute the test and assert the expected results
@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

Outcomes