Skip to content

Commit

Permalink
refactor: changed test case execution to work on flow scope level
Browse files Browse the repository at this point in the history
this improves the testing of certain BPMN constructs and enables parallel multi instance tests
  • Loading branch information
gclaussn committed Jan 4, 2025
1 parent d398689 commit dfbe1bb
Show file tree
Hide file tree
Showing 52 changed files with 1,661 additions and 958 deletions.
2 changes: 1 addition & 1 deletion gradle-plugin-8/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ The integration tests are implemented using the [Robot Framework](https://robotf
To execute unit and integration tests, run:

```sh
mvn clean integration-test -pl gradle-plugin-8 -am
mvn clean install -pl gradle-plugin-8 -am
```

The Robot test report is written to `target/robot/report.html`.
2 changes: 1 addition & 1 deletion gradle-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ The integration tests are implemented using the [Robot Framework](https://robotf
To execute unit and integration tests, run:

```sh
mvn clean integration-test -pl gradle-plugin -am
mvn clean install -pl gradle-plugin -am
```

The Robot test report is written to `target/robot/report.html`.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@
import java.util.ArrayList;
import java.util.List;

import org.camunda.community.bpmndt.model.BpmnElement;
import org.camunda.community.bpmndt.model.BpmnElementScope;
import org.camunda.community.bpmndt.model.TestCase;

public class TestCaseContext {

private final List<BpmnElement> multiInstances = new ArrayList<>();
private final List<BpmnElementScope> multiInstanceScopes = new ArrayList<>();
private final List<GeneratorStrategy> strategies = new ArrayList<>();

private String className;
Expand All @@ -20,14 +16,6 @@ public class TestCaseContext {
private String resourceName;
private TestCase testCase;

public void addMultiInstance(BpmnElement element) {
multiInstances.add(element);
}

public void addMultiInstanceScope(BpmnElementScope scope) {
multiInstanceScopes.add(scope);
}

public void addStrategy(GeneratorStrategy strategy) {
strategies.add(strategy);
}
Expand All @@ -41,14 +29,6 @@ public String getClassName() {
return className;
}

public List<BpmnElement> getMultiInstances() {
return multiInstances;
}

public List<BpmnElementScope> getMultiInstanceScopes() {
return multiInstanceScopes;
}

public String getName() {
return name;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;

import org.camunda.community.bpmndt.api.TestCaseInstanceElement.CallActivityElement;
import org.camunda.community.bpmndt.api.TestCaseInstanceMemo.ProcessInstanceMemo;

import io.camunda.zeebe.process.test.assertions.BpmnAssert;
import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert;
Expand Down Expand Up @@ -61,7 +63,9 @@ public CallActivityHandler(CallActivityElement element) {
this.element = element;
}

void apply(TestCaseInstance instance, long processInstanceKey) {
void apply(TestCaseInstance instance, long flowScopeKey) {
var processInstanceKey = instance.getProcessInstanceKey(flowScopeKey);

if (verifier != null) {
verifier.accept(new ProcessInstanceAssert(processInstanceKey, BpmnAssert.getRecordStream()));
}
Expand All @@ -70,7 +74,7 @@ void apply(TestCaseInstance instance, long processInstanceKey) {
processIdExpressionConsumer.accept(element.processId);
}

var calledProcessInstance = instance.getCalledProcessInstance(processInstanceKey, element.id);
var calledProcessInstance = getCalledProcessInstance(instance, flowScopeKey);
var job = instance.getJob(calledProcessInstance.key, SIMULATE_ELEMENT_ID);

if (expectedProcessId != null && !expectedProcessId.equals(calledProcessInstance.bpmnProcessId)) {
Expand Down Expand Up @@ -116,7 +120,7 @@ void apply(TestCaseInstance instance, long processInstanceKey) {
instance.getClient().newThrowErrorCommand(job.key).errorCode(DO_ESCALATION_CODE).send().join();
}

instance.hasTerminated(processInstanceKey, element.id);
instance.hasTerminated(flowScopeKey, element.id);
} else {
// end called process instance
if (variables != null) {
Expand All @@ -125,7 +129,7 @@ void apply(TestCaseInstance instance, long processInstanceKey) {
instance.getClient().newCompleteCommand(job.key).variables(variableMap).send().join();
}

instance.hasPassed(processInstanceKey, element.id);
instance.hasPassed(flowScopeKey, element.id);
}

if (outputVerifier != null) {
Expand Down Expand Up @@ -314,4 +318,30 @@ public CallActivityHandler withErrorCode(String errorCode) {
this.errorCode = errorCode;
return this;
}

private ProcessInstanceMemo getCalledProcessInstance(TestCaseInstance instance, long flowScopeKey) {
return instance.select(memo -> {
var callActivity = memo.elements.stream().filter(e ->
e.flowScopeKey == flowScopeKey && Objects.equals(e.id, element.id)
).findFirst();

if (callActivity.isEmpty()) {
var message = String.format("call activity %s of flow scope %d could not be found", element.id, flowScopeKey);
throw instance.createException(message, flowScopeKey);
}

var callActivityKey = callActivity.get().key;

var calledProcessInstance = memo.processInstances.stream().filter(
processInstance -> processInstance.parentElementInstanceKey == callActivityKey
).findFirst();

if (calledProcessInstance.isEmpty()) {
var message = String.format("call activity %s of flow scope %d has not called a process", element.id, flowScopeKey);
throw instance.createException(message, flowScopeKey);
}

return calledProcessInstance.get();
});
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package org.camunda.community.bpmndt.api;

import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.camunda.community.bpmndt.api.TestCaseInstanceElement.MultiInstanceElement;
import org.camunda.community.bpmndt.api.TestCaseInstanceMemo.ElementMemo;

import io.camunda.zeebe.process.test.assertions.BpmnAssert;
import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert;
import io.camunda.zeebe.protocol.record.intent.ProcessInstanceIntent;

/**
* Fluent API to handle multi instances and multi instance scopes, using custom code - see {@link #execute(BiConsumer)}.
Expand All @@ -17,7 +22,9 @@ public class CustomMultiInstanceHandler {

private Consumer<ProcessInstanceAssert> verifier;
private BiConsumer<TestCaseInstance, Long> action;
private BiConsumer<TestCaseInstance, Long> loopAction;

private Integer expectedLoopCount;
private Boolean expectedSequential;

public CustomMultiInstanceHandler(String elementId) {
Expand All @@ -40,8 +47,9 @@ public CustomMultiInstanceHandler(MultiInstanceElement element) {
this.element = element;
}

void apply(TestCaseInstance instance, long processInstanceKey) {
void apply(TestCaseInstance instance, long flowScopeKey) {
if (verifier != null) {
var processInstanceKey = instance.getProcessInstanceKey(flowScopeKey);
verifier.accept(new ProcessInstanceAssert(processInstanceKey, BpmnAssert.getRecordStream()));
}

Expand All @@ -51,8 +59,24 @@ void apply(TestCaseInstance instance, long processInstanceKey) {
}

if (action != null) {
var processInstanceKey = instance.getProcessInstanceKey(flowScopeKey);
action.accept(instance, processInstanceKey);
}

int loopIndex = 0;

Optional<ElementMemo> next;
while ((next = next(instance, flowScopeKey, loopIndex)).isPresent()) {
loopIndex++;

if (loopAction != null) {
loopAction.accept(instance, next.get().key);
}
}

if (expectedLoopCount != null && expectedLoopCount != loopIndex) {
throw new AssertionError(String.format("expected multi instance %s to loop %dx, but was %dx", element.id, expectedLoopCount, loopIndex));
}
}

/**
Expand Down Expand Up @@ -82,9 +106,38 @@ public void execute(BiConsumer<TestCaseInstance, Long> action) {
if (action == null) {
throw new IllegalArgumentException("action is null");
}
if (loopAction != null) {
throw new IllegalStateException("either use action or loop action");
}
this.action = action;
}

/**
* Executes a custom action that handles the multi instance loop. The given consumer is called once per loop.
* <p>
* Please note: If the multi instance is <b>NOT</b> a scope (e.g. embedded sub process), the flow scope key, used to apply handler or to check if an element
* has been passed, must be obtained via {@link TestCaseInstance#getFlowScopeKey(long)}.
* <pre>
* tc.handleUserTask().executeLoop((instance, elementInstanceKey) -> {
* var flowScopeKey = instance.getFlowScopeKey(elementInstanceKey);
*
* instance.apply(flowScopeKey, userTaskHandler);
* instance.hasPassed(flowScopeKey, "userTask");
* }
* </pre>
*
* @param loopAction A specific action that accepts a {@link TestCaseInstance} and the current element instance key.
*/
public void executeLoop(BiConsumer<TestCaseInstance, Long> loopAction) {
if (loopAction == null) {
throw new IllegalArgumentException("loop action is null");
}
if (action != null) {
throw new IllegalStateException("either use action or loop action");
}
this.loopAction = loopAction;
}

/**
* Verifies the multi instance state.
* <p>
Expand All @@ -98,6 +151,17 @@ public CustomMultiInstanceHandler verify(Consumer<ProcessInstanceAssert> verifie
return this;
}

/**
* Verifies that the multi instance loop is executed n-times.
*
* @param expectedLoopCount The expected loop count at the point of time when the multi instance is left (completed or terminated by a boundary event).
* @return The handler.
*/
public CustomMultiInstanceHandler verifyLoopCount(int expectedLoopCount) {
this.expectedLoopCount = expectedLoopCount;
return this;
}

/**
* Verifies that the multi instance loop execution is done in parallel.
*
Expand All @@ -121,4 +185,38 @@ public CustomMultiInstanceHandler verifySequential() {
private String getText(boolean sequential) {
return sequential ? "sequential" : "parallel";
}

private Optional<ElementMemo> next(TestCaseInstance instance, long flowScopeKey, int loopIndex) {
return instance.select(memo -> {
var multiInstanceElements = memo.multiInstanceElements.stream().filter(e ->
e.flowScopeKey == flowScopeKey && Objects.equals(e.id, element.id)
).collect(Collectors.toList());

if (multiInstanceElements.isEmpty()) {
var message = String.format("expected flow scope %d to have multi instance element %s, but has not", flowScopeKey, element.id);
throw instance.createException(message, flowScopeKey);
}

var multiInstanceKey = multiInstanceElements.get(0).key;

var elements = memo.elements.stream().filter(e ->
e.flowScopeKey == multiInstanceKey
&& Objects.equals(e.id, element.id)
&& e.state == ProcessInstanceIntent.ELEMENT_ACTIVATED
).collect(Collectors.toList());

if (elements.size() > loopIndex) {
// multi instance has next element
return Optional.of(elements.get(loopIndex));
}

if (multiInstanceElements.size() == 2) {
// multi instance is completed or terminated
return Optional.empty();
}

var message = String.format("expected multi instance %s of flow scope %d to be completed or terminated, but was not", element.id, flowScopeKey);
throw instance.createException(message, flowScopeKey);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ public JobHandler(JobElement element) {
this.element = element;
}

void apply(TestCaseInstance instance, long processInstanceKey) {
void apply(TestCaseInstance instance, long flowScopeKey) {
if (verifier != null) {
var processInstanceKey = instance.getProcessInstanceKey(flowScopeKey);
verifier.accept(new ProcessInstanceAssert(processInstanceKey, BpmnAssert.getRecordStream()));
}

Expand All @@ -71,7 +72,7 @@ void apply(TestCaseInstance instance, long processInstanceKey) {
typeExpressionConsumer.accept(element.type);
}

var job = instance.getJob(processInstanceKey, element.id);
var job = instance.getJob(flowScopeKey, element.id);

if (expectedRetries != null && !expectedRetries.equals(job.retries)) {
var message = "expected job %s to have a retry count of %d, but was %d";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ public MessageEventHandler(MessageEventElement element) {
correlate();
}

void apply(TestCaseInstance instance, long processInstanceKey) {
void apply(TestCaseInstance instance, long flowScopeKey) {
if (verifier != null) {
var processInstanceKey = instance.getProcessInstanceKey(flowScopeKey);
verifier.accept(new ProcessInstanceAssert(processInstanceKey, BpmnAssert.getRecordStream()));
}

Expand All @@ -68,7 +69,7 @@ void apply(TestCaseInstance instance, long processInstanceKey) {
messageNameExpressionConsumer.accept(element.messageName);
}

var messageSubscription = instance.getMessageSubscription(processInstanceKey, element.id);
var messageSubscription = instance.getMessageSubscription(flowScopeKey, element.id);

if (expectedCorrelationKey != null && !expectedCorrelationKey.equals(messageSubscription.correlationKey)) {
var message = "expected message event %s to have correlation key '%s', but was '%s'";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ public OutboundConnectorHandler(OutboundConnectorElement element) {
complete();
}

void apply(TestCaseInstance instance, long processInstanceKey) {
void apply(TestCaseInstance instance, long flowScopeKey) {
if (verifier != null) {
var processInstanceKey = instance.getProcessInstanceKey(flowScopeKey);
verifier.accept(new ProcessInstanceAssert(processInstanceKey, BpmnAssert.getRecordStream()));
}

Expand All @@ -84,7 +85,7 @@ void apply(TestCaseInstance instance, long processInstanceKey) {
outputMappingConsumer.accept(element.outputs);
}

var job = instance.getJob(processInstanceKey, element.id);
var job = instance.getJob(flowScopeKey, element.id);

if (expectedRetries != null && !expectedRetries.equals(job.retries)) {
var message = "expected job %s to have a retry count of %d, but was %d";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,9 @@ public ReceiveTaskHandler(MessageEventElement element) {
correlate();
}

void apply(TestCaseInstance instance, long processInstanceKey) {
void apply(TestCaseInstance instance, long flowScopeKey) {
if (verifier != null) {
var processInstanceKey = instance.getProcessInstanceKey(flowScopeKey);
verifier.accept(new ProcessInstanceAssert(processInstanceKey, BpmnAssert.getRecordStream()));
}

Expand All @@ -69,7 +70,7 @@ void apply(TestCaseInstance instance, long processInstanceKey) {
messageNameExpressionConsumer.accept(element.messageName);
}

var messageSubscription = instance.getMessageSubscription(processInstanceKey, element.id);
var messageSubscription = instance.getMessageSubscription(flowScopeKey, element.id);

if (expectedCorrelationKey != null && !expectedCorrelationKey.equals(messageSubscription.correlationKey)) {
var message = "expected message event %s to have correlation key '%s', but was '%s'";
Expand Down
Loading

0 comments on commit dfbe1bb

Please sign in to comment.