cancel
Showing results for 
Search instead for 
Did you mean: 

Asynchronous User Task and Mocks

jwglista
Champ in-the-making
Champ in-the-making
Hello,

I've had success using the MockExpressionManager to mock delegate methods for Service Tasks, as well as task listeners.  I have a user task that is asynchronous, and I am attempting to mock a listener method on this user task for the create event.  From my understanding, this requires the use of JobTestHelper.waitForJobExecutorToProcessAllJobs so that the asynchronous user task can complete before verifying any of the mocks or other behavior.  However, I'm finding that when using this method, the Activiti Engine can not resolve the mocked object, even though it has been registered with the MockExpressionManager using the Mocks.register() method.

Does anyone have any suggestions on this?

Thanks

<b>Java Test class (snippet):</b>

   @Mock
   private TestMockDelegate testMockDelegateMock;
   
   @Mock
   private TestMockListener testMockListenerMock;

   @Before
   public void setup() throws Exception {
      MockitoAnnotations.initMocks(this);
      SpringProcessEngineConfiguration configuration = (SpringProcessEngineConfiguration) processEngine.getProcessEngineConfiguration();
      configuration.setExpressionManager(new MockExpressionManager());
   }

   @Test
   public void testWithMock() throws Exception {
      System.out.println("================= testWithMock() start =================");
      Mocks.register("testMockDelegate", testMockDelegateMock);
      Mocks.register("testMockListener", testMockListenerMock);
      when(testMockDelegateMock.execute((DelegateExecution) anyObject())).thenReturn("mock execute() was used");
      ProcessInstance pi = runtimeService.startProcessInstanceByKey("testMockProcess");
      assertNotNull(pi);
      
      JobTestHelper.waitForJobExecutorToProcessAllJobs(processEngine.getProcessEngineConfiguration(), managementService, 10000L, 100L);
      
      verify(testMockDelegateMock, times(1)).execute((DelegateExecution) anyObject());
      verify(testMockListenerMock, times(1)).execute((ActivityExecution) anyObject());
      verify(testMockListenerMock, times(1)).notify((DelegateTask) anyObject());
      verifyNoMoreInteractions(testMockDelegateMock);
      verifyNoMoreInteractions(testMockListenerMock);
      System.out.println("================= testWithMock() end  ================= \n");
   }


<b>User task as defined in the process definition:</b>

    <userTask id="usertask1" name="User Task" activiti:async="true">
      <extensionElements>
        <activiti:taskListener event="create" expression="${testMockListener.notify(task)}"></activiti:taskListener>
      </extensionElements>
    </userTask>


<b>The listener class:</b>

public class TestMockListener {
   
   private static final Logger log = Logger.getLogger(TestMockListener.class);
   
   public void notify(DelegateTask task)  {
      System.out.println("TestMockListener.notify() has been executed");
   }
   
   public void execute(ActivityExecution execution) {
      System.out.println("TestMockListener.execute() has been executed");
   }
}


<b>The exception: </b>

================= testWithMock() start =================
mock execute() was used
2016-09-07 08:18:45 ERROR ExecuteJobsRunnable:96 - exception during job execution: Job 45 failed
org.activiti.engine.ActivitiException: Job 45 failed
   at org.activiti.engine.impl.cmd.ExecuteJobsCmd.execute(ExecuteJobsCmd.java:110)
   at org.activiti.engine.impl.interceptor.CommandInvoker.execute(CommandInvoker.java:24)
   at org.activiti.engine.impl.interceptor.CommandContextInterceptor.execute(CommandContextInterceptor.java:57)
   at org.activiti.spring.SpringTransactionInterceptor$1.doInTransaction(SpringTransactionInterceptor.java:47)
   at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:133)
   at org.activiti.spring.SpringTransactionInterceptor.execute(SpringTransactionInterceptor.java:45)
   at org.activiti.engine.impl.interceptor.LogInterceptor.execute(LogInterceptor.java:31)
   at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:40)
   at org.activiti.engine.impl.cfg.CommandExecutorImpl.execute(CommandExecutorImpl.java:35)
   at org.activiti.engine.impl.jobexecutor.ExecuteJobsRunnable.handleMultipleJobs(ExecuteJobsRunnable.java:94)
   at org.activiti.engine.impl.jobexecutor.ExecuteJobsRunnable.run(ExecuteJobsRunnable.java:49)
   at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
   at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
   at java.lang.Thread.run(Thread.java:745)
Caused by: org.activiti.engine.ActivitiException: Exception while invoking TaskListener: Unknown property used in expression: ${testMockListener.notify(task)}
   at org.activiti.engine.impl.persistence.entity.TaskEntity.fireEvent(TaskEntity.java:742)
   at org.activiti.engine.impl.bpmn.behavior.UserTaskActivityBehavior.execute(UserTaskActivityBehavior.java:213)
   at org.activiti.engine.impl.pvm.runtime.AtomicOperationActivityExecute.execute(AtomicOperationActivityExecute.java:60)
   at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
   at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:655)
   at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:650)
   at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionNotifyListenerStart.eventNotificationsCompleted(AtomicOperationTransitionNotifyListenerStart.java:52)
   at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:56)
   at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
   at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:655)
   at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:650)
   at org.activiti.engine.impl.pvm.runtime.AbstractEventAtomicOperation.execute(AbstractEventAtomicOperation.java:49)
   at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
   at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperationSync(ExecutionEntity.java:655)
   at org.activiti.engine.impl.persistence.entity.ExecutionEntity.performOperation(ExecutionEntity.java:650)
   at org.activiti.engine.impl.pvm.runtime.AtomicOperationTransitionCreateScope.execute(AtomicOperationTransitionCreateScope.java:49)
   at org.activiti.engine.impl.interceptor.CommandContext.performOperation(CommandContext.java:97)
   at org.activiti.engine.impl.jobexecutor.AsyncContinuationJobHandler.execute(AsyncContinuationJobHandler.java:35)
   at org.activiti.engine.impl.persistence.entity.JobEntity.execute(JobEntity.java:85)
   at org.activiti.engine.impl.persistence.entity.MessageEntity.execute(MessageEntity.java:34)
   at org.activiti.engine.impl.cmd.ExecuteJobsCmd.execute(ExecuteJobsCmd.java:88)
   … 13 more
Caused by: org.activiti.engine.ActivitiException: Unknown property used in expression: ${testMockListener.notify(task)}
   at org.activiti.engine.impl.el.JuelExpression.getValue(JuelExpression.java:53)
   at org.activiti.engine.impl.bpmn.listener.ExpressionTaskListener.notify(ExpressionTaskListener.java:33)
   at org.activiti.engine.impl.delegate.TaskListenerInvocation.invoke(TaskListenerInvocation.java:34)
   at org.activiti.engine.impl.delegate.DelegateInvocation.proceed(DelegateInvocation.java:37)
   at org.activiti.engine.impl.delegate.DefaultDelegateInterceptor.handleInvocation(DefaultDelegateInterceptor.java:25)
   at org.activiti.engine.impl.persistence.entity.TaskEntity.fireEvent(TaskEntity.java:738)
   … 33 more
Caused by: org.activiti.engine.impl.javax.el.PropertyNotFoundException: Cannot resolve identifier 'testMockListener'
   at org.activiti.engine.impl.juel.AstIdentifier.eval(AstIdentifier.java:83)
   at org.activiti.engine.impl.juel.AstMethod.invoke(AstMethod.java:90)
   at org.activiti.engine.impl.juel.AstMethod.eval(AstMethod.java:86)
   at org.activiti.engine.impl.juel.AstEval.eval(AstEval.java:50)
   at org.activiti.engine.impl.juel.AstNode.getValue(AstNode.java:26)
   at org.activiti.engine.impl.juel.TreeValueExpression.getValue(TreeValueExpression.java:114)
   at org.activiti.engine.impl.delegate.ExpressionGetInvocation.invoke(ExpressionGetInvocation.java:33)
   at org.activiti.engine.impl.delegate.DelegateInvocation.proceed(DelegateInvocation.java:37)
   at org.activiti.engine.impl.delegate.DefaultDelegateInterceptor.handleInvocation(DefaultDelegateInterceptor.java:25)
   at org.activiti.engine.impl.el.JuelExpression.getValue(JuelExpression.java:48)
   … 38 more
3 REPLIES 3

jwglista
Champ in-the-making
Champ in-the-making
Ok I'll give everyone a clue.  It looks like the org.activiti.engine.test.mock.Mocks class is using ThreadLocal to store the Map of mocks.  This will not work with async user tasks, since the process for those would be starting in a separate thread.  Not sure if this will require a change in the Activiti code going forward, or if there is a way I can get around this.

<code>
public class Mocks {

  private static ThreadLocal<Map<String, Object>> mockContainer = new ThreadLocal<Map<String, Object>>();

  private static Map<String, Object> getMocks() {
    Map<String, Object> mocks = mockContainer.get();
    if (mocks == null) {
      mocks = new HashMap<String, Object>();
      Mocks.mockContainer.set(mocks);
    }
    return mocks;
  }
</code>

trademak
Star Contributor
Star Contributor
The mock implementation isn't yet suitable for asynchronous service tasks. So that will need some changes to the mock classes.

Best regards,

jwglista
Champ in-the-making
Champ in-the-making
Thanks, Tijs.  I ended up just creating a few classes to override org.activiti.engine.test.mock.Mocks so that it is not using a ThreadLocal object.  This made the mocks global to the JVM through a static instance of a map.  Not the most elegant solution, but it has worked perfectly.

Created the class <b> MocksGlobal: </b>
<code>
public class MocksGlobal extends Mocks {

private static Map<String, Object> mockContainer = new HashMap<String, Object>();

@SuppressWarnings("unused")
private static Map<String, Object> getMocks() {
  Map<String, Object> mocks = mockContainer;
  if (mocks == null) {
   mockContainer = new HashMap<String, Object>();
  }
  return mocks;
}

/**
  * This method lets you register a mock object. Make sure to register the
  * {@link MockExpressionManager} with your process engine configuration.
  *
  * @param key
  *            the key under which the mock object will be registered
  * @param value
  *            the mock object
  */
public static void register(String key, Object value) {
  getMocks().put(key, value);
}

/**
  * This method returns the mock object registered under the provided key or
  * null if there is no object for the provided key.
  *
  * @param key
  *            the key of the requested object
  * @return the mock object registered under the provided key or null if
  *         there is no object for the provided key
  */
public static Object get(Object key) {
  return getMocks().get(key);
}

/**
  * This method resets the internal map of mock objects.
  */
public static void reset() {
  if (getMocks() != null) {
   getMocks().clear();
  }
}
}
</code>

Created a new class, <b>MockGlobalExpressionManager</b>.  This uses another new class, <b>MockElGlobalResolver</b> which is shown further down:

<code>
public class MockGlobalExpressionManager extends ExpressionManager {
   @Override
   protected ELResolver createElResolver(VariableScope variableScope) {
     CompositeELResolver compositeElResolver = new CompositeELResolver();
     compositeElResolver.add(new VariableScopeElResolver(variableScope));
     compositeElResolver.add(new MockElGlobalResolver());
     compositeElResolver.add(new ArrayELResolver());
     compositeElResolver.add(new ListELResolver());
     compositeElResolver.add(new MapELResolver());
     compositeElResolver.add(new BeanELResolver());
     return compositeElResolver;
   }
}
</code>

The <b>MockElGlobalResolver</b> class that uses the new <b>MocksGlobal</b> class to resolve mocks:

<code>
public class MockElGlobalResolver extends MockElResolver {
   @Override
   public Object getValue(ELContext context, Object base, Object property) {
     Object bean = MocksGlobal.get(property);
     if (bean != null) {
       context.setPropertyResolved(true);
     }
     return bean;
   }
}
</code>

Finally, in our unit test class, we now register <b>MockGlobalExpressionManager</b> instead of the Activiti MockExpressionManager:

<code>
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
SpringProcessEngineConfiguration configuration = (SpringProcessEngineConfiguration) processEngine.getProcessEngineConfiguration();
configuration.setExpressionManager(new MockGlobalExpressionManager());
}
</code>

Then in our test, we use the <b>MocksGlobal</b> class to register the mocks:

<code>
@Test
public void testWithMock() throws Exception {
System.out.println("================= testWithMock() start =================");
MocksGlobal.register("testMockDelegate", testMockDelegateMock);
MocksGlobal.register("testMockListener", testMockListenerMock);

when(testMockDelegateMock.execute((DelegateExecution) anyObject())).thenReturn("mock execute() was used");
ProcessInstance pi = runtimeService.startProcessInstanceByKey("testMockProcess");
assertNotNull(pi);

JobTestHelper.waitForJobExecutorToProcessAllJobs(processEngine.getProcessEngineConfiguration(), managementService, 180000L, 100L);

verify(testMockDelegateMock, times(1)).execute((DelegateExecution) anyObject());
verify(testMockListenerMock, times(1)).execute((ActivityExecution) anyObject());
verify(testMockListenerMock, times(1)).notify((DelegateTask) anyObject());
verifyNoMoreInteractions(testMockDelegateMock);
verifyNoMoreInteractions(testMockListenerMock);
System.out.println("================= testWithMock() end  ================= \n");
}
</code>