Closed StephenOTT closed 5 years ago
Or here is another example:
class HamcrestTestsSpec extends Specification{
@Shared BpmnModelInstance model
def setupSpec(){
model = prepModelForCoverage('bpmn/conditionalstart/HamcrestTests.bpmn') withTraits(redundantSequenceFlows)
}
def 'Redundant Sequence Flows'() {
when: 'Given a Model Definition'
then: 'Ensure there are no redundant sequence flows'
// assertThat model.doesNotContainRedundantSequenceFlows(["StartEvent_1ugw7xf":"Task_1qb2k3a"])
assertThat model.doesNotContainRedundantSequenceFlows()
}
}
which will throw the following assert exception:
assertThat model.doesNotContainRedundantSequenceFlows()
| |
| java.lang.AssertionError:
| Found duplicate(s):
| <[{"StartEvent_1ugw7xf"="Task_1qb2k3a"}]>
| in:
| <[{"StartEvent_1ugw7xf"="Task_1qb2k3a"}, {"StartEvent_1ugw7xf"="Task_1qb2k3a"}, {"Task_1qb2k3a"="EndEvent_10ag13t"}]>
BpmnModelInstanceImpl1_groovyProxy@62d363ab
And if the commented out line is used instead, the assertion will pass due to the ["StartEvent_1ugw7xf":"Task_1qb2k3a"]
redundancy being ignored.
key in this example is the usage of:
model = prepModelForCoverage('bpmn/conditionalstart/HamcrestTests.bpmn') withTraits(redundantSequenceFlows)
which lets us apply the traits to the specific model in question. This has interesting implications for running different combinations of assertions against multiple models.
To support this functionality, the trait was updated to:
trait redundantSequenceFlows {
// https://docs.camunda.org/javadoc/camunda-bpm-platform/7.8/?org/camunda/bpm/model/bpmn/instance/package-summary.html
def doesNotContainRedundantSequenceFlows(Map<String,String> ignoredSequenceFlows = [:]){
Collection<SequenceFlow> sequenceFlows = this.getModelElementsByType(SequenceFlow.class)
ArrayList targetSource = sequenceFlows.collect {
[(it.getSource().getId()) : it.getTarget().getId()]
}
targetSource.removeAll(ignoredSequenceFlows)
assertThat(targetSource).doesNotHaveDuplicates()
}
}
The idea/point of using traits here is to provide flexibility and easy readability + the ability to "technical" analysts to create new traits / re-usable assertions that match their specific use cases.
Hey,
thank you for your interest! We need to think about this - the Spock approach may also be interesting for @larsbe to constrain data flows, right?
Best regards, Frank
Hey
so here is an updated example for large simplification using traits for Message Events:
class MessageEventsSpec extends Specification{
@Shared BpmnModelInstance model
def setupSpec(){
String path = 'bpmn/qa-test.bpmn'
InputStream bpmnFile = this.class.getResource(path.toString()).newInputStream()
model = Bpmn.readModelFromStream(bpmnFile).withTraits(bpmnEvents)
}
@Unroll
def "Message Events Check: External Implementation"(){
expect:
verifyAll {
that(event['implementation']['topic'], notNullValue())
that(event['eventId'], notNullValue())
that(event['eventName'], notNullValue())
}
where:
event << model.getMessageEvents().findAll { it['implementation']['camundaType'] == 'external'}
// event << model.getMessageEvents('endEvent')
}
trait bpmnEvents{
List<Map<String,Object>> getMessageEvents(String activityType = null, List <String> ignoredMessageEvents = []){
BpmnModelInstance model = (BpmnModelInstance)this
Collection<MessageEventDefinition> messageEventDefinitions = model.getModelElementsByType(MessageEventDefinition.class)
if (activityType != null){
messageEventDefinitions = messageEventDefinitions.findAll {
it.getParentElement().getElementType().getTypeName() == activityType
}
}
List<Map<String,Object>> events = messageEventDefinitions.collect {
[
('activityType') : it.getParentElement().getElementType().getTypeName(),
('activityId') : it.getParentElement().getAttributeValue('id'),
('eventId') : it.getMessage()?.getId() ?: null,
('eventName') : it.getMessage()?.getName() ?: null,
('implementation') : [
('delegateExpression') : it.getCamundaDelegateExpression(),
('camundaType') : it.getCamundaType(),
('expression') : it.getCamundaExpression(),
('resultVariable') : it.getCamundaResultVariable(),
('topic') : it.getCamundaTopic(),
('operation') : it.getOperation(),
('extension') : it.getExtensionElements()?.getElementsQuery()?.filterByType(CamundaConnector.class)?.singleResult()?.getElementType()?.getTypeName()
]
]
}
events.removeAll{
ignoredMessageEvents.any {it['activityId']}
}
println JsonOutput.prettyPrint(JsonOutput.toJson(events))
return events
}
}
}
you will get output similar to:
that(event['implementation']['topic'], notNullValue())
| | | |
false| | null
| [delegateExpression:null, camundaType:external, expression:null, resultVariable:null, topic:null, operation:null, extension:null]
[activityType:intermediateThrowEvent, activityId:message_throw, eventId:Message_1jggs9q, eventName:null, implementation:[delegateExpression:null, camundaType:external, expression:null, resultVariable:null, topic:null, operation:null, extension:null]]
Expected: not null
but: was null
Run 2: HamcrestTestsSpec.Message Events Check: External Implementation:31->Specification.verifyAll:223->Message Events Check: External Implementation_closure1:-1->Message Events Check: External Implementation_closure1:34 Condition not satisfied:
that(event['eventName'], notNullValue())
| | |
false| null
[activityType:intermediateThrowEvent, activityId:message_throw, eventId:Message_1jggs9q, eventName:null, implementation:[delegateExpression:null, camundaType:external, expression:null, resultVariable:null, topic:null, operation:null, extension:null]]
Expected: not null
but: was null
Run 3: HamcrestTestsSpec.Message Events Check: External Implementation:31->Specification.verifyAll:223->Message Events Check: External Implementation_closure1:-1->Message Events Check: External Implementation_closure1:34 Condition failed with Exception:
that(event['eventName'], notNullValue())
| | |
| null not null
[activityType:intermediateThrowEvent, activityId:message_throw, eventId:Message_1jggs9q, eventName:null, implementation:[delegateExpression:null, camundaType:external, expression:null, resultVariable:null, topic:null, operation:null, extension:null]]
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0
and using the definition:
...
def "Message Events Check: External Implementation"(){
expect:
verifyAll {
that(event['implementation']['topic'], notNullValue())
that(event['eventId'], notNullValue())
that(event['eventName'], notNullValue())
}
where:
event << model.getMessageEvents().findAll { it['implementation']['camundaType'] == 'external'}
...
Analysts can quickly create reusable traits for various checks and you can layer your traits to inherit much deeper than with regular diamond issues.
Here is the output of what the getMessageEvents() method will return:
[
{
"activityType": "startEvent",
"activityId": "message_start",
"eventId": "Message_0d14biw",
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "intermediateCatchEvent",
"activityId": "message_catch",
"eventId": "Message_1jggs9q",
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "intermediateThrowEvent",
"activityId": "message_throw",
"eventId": "Message_1jggs9q",
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": "external",
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "endEvent",
"activityId": "message_end",
"eventId": null,
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "startEvent",
"activityId": "sub-message-start",
"eventId": "Message_0gyzbq3",
"eventName": "5555",
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "endEvent",
"activityId": "sub-message-end",
"eventId": null,
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "startEvent",
"activityId": "sub-event-start",
"eventId": "Message_1uudc2w",
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "startEvent",
"activityId": "sub-event-start-non",
"eventId": "Message_0gyzbq3",
"eventName": "5555",
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "endEvent",
"activityId": "sub-event-end",
"eventId": "Message_0d14biw",
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "boundaryEvent",
"activityId": "sub-event-boundary",
"eventId": "Message_0gyzbq3",
"eventName": "5555",
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "boundaryEvent",
"activityId": "sub-event-non-boundary",
"eventId": "Message_1jggs9q",
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "boundaryEvent",
"activityId": "boundary-throw",
"eventId": null,
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "intermediateCatchEvent",
"activityId": "message_catch_post-event",
"eventId": null,
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
},
{
"activityType": "boundaryEvent",
"activityId": "boundary-non-throw",
"eventId": "Message_1jggs9q",
"eventName": null,
"implementation": {
"delegateExpression": null,
"camundaType": null,
"expression": null,
"resultVariable": null,
"topic": null,
"operation": null,
"extension": null
}
}
]
There is a small bug with "veryifyAll{}" with spock that is preventing the "assert" keywork to be used.
But once fixed you can do things like: assert that(event['implementation']['topic'], notNullValue()), "A Topic is required when using a External implementation in a Message Event"
, which will output a custom business focused error message that can be reported on.
This pattern is powerful because it drastically simplifies the architecture and the tester's ability to tailor the tests on a per project basis. Where the equiv of this code in vPav is: https://github.com/viadee/vPAV/blob/master/src/main/java/de/viadee/bpm/vPAV/processing/checker/MessageEventChecker.java
Here is a update for Timer evaluation:
trait bpmnTimers{
Collection<TimerEventDefinition> getTimers(){
BpmnModelInstance model = (BpmnModelInstance)this
Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
return timerEventDefinitions
}
List<Date> evaluateTimers() {
BpmnModelInstance model = (BpmnModelInstance) this
Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
List<Date> timers = new ArrayList<Date>()
timerEventDefinitions.each { timer ->
Date timerEval = evaluateTimer(timer)
assert timerEval instanceof Date
timers << timerEval
}
return timers
}
Date evaluateTimer(TimerEventDefinition timer){
Map<String,String> timerInfo = getTimerValue(timer)
switch (timerInfo) {
case { it.type == 'date'}:
DueDateBusinessCalendar dueDateCalendar = new DueDateBusinessCalendar()
Date dueDate = dueDateCalendar.resolveDuedate(timerInfo.value)
return dueDate
case {it.type == 'cycle'}:
CycleBusinessCalendar cycleBusinessCalendar = new CycleBusinessCalendar()
Date cycleDueDate = cycleBusinessCalendar.resolveDuedate(timerInfo.value)
return cycleDueDate
case {it.type == 'duration'}:
DurationBusinessCalendar durationCalendar = new DurationBusinessCalendar()
Date durationDueDate = durationCalendar.resolveDuedate(timerInfo.value)
return durationDueDate
default:
throw new IOException('Invalid Timer mapping found: must be of type: date or cycle or duration')
}
}
Map<String, String> getTimerValue(TimerEventDefinition timer) {
if (timer.getTimeDate() != null) {
return [('type'):'date',
('value'): timer.getTimeDate().getRawTextContent()]
} else if (timer.getTimeCycle() != null) {
return [('type'):'cycle',
('value'): timer.getTimeCycle().getRawTextContent()]
} else if (timer.getTimeDuration() != null) {
return [('type'):'duration',
('value'): timer.getTimeDuration().getRawTextContent()]
} else {
throw new IOException('Timer definition missing; Timer definition is required on all timers')
}
}
}
You can leverage the flow through something like:
...
@Shared BpmnModelInstance model
def setupSpec(){
String path = 'bpmn/qa-test.bpmn'
InputStream bpmnFile = this.class.getResource(path.toString()).newInputStream()
model = Bpmn.readModelFromStream(bpmnFile).withTraits(bpmnEvents, bpmnTimers)
}
def 'test timers'(){
when: 'loaded a process'
then:
println model.evaluateTimers()
}
...
This will print each of the timers Dates that were evaluated [Wed Jun 27 13:30:00 EDT 2018]
or throw a validation exception by the Business Calendar engines or some of the extra logic for pre-config issues
println model.evaluateTimers()
| |
| org.camunda.bpm.engine.ProcessEngineException: ENGINE-09026 Exception while parsing cron expression '0 0/5 * *': Unexpected end of expression.
BpmnModelInstanceImpl1_groovyProxy@64a8c844
Further error messaging can be added for things like what specific activityId had the error.
x-ref for vPav timer evals: https://github.com/viadee/vPAV/blob/master/src/main/java/de/viadee/bpm/vPAV/processing/checker/TimerExpressionChecker.java
Here is updated version of Timer evaluations with ability to provide a custom Time for time simulation
...
def 'test timers'(){
when: 'loaded a process'
then:
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy MM dd - HH:mm")
Date customTime = simpleDateFormat.parse("2011 03 11 - 17:23")
println model.evaluateTimers(customTime)
}
...
and then:
trait bpmnTimers{
// reference: https://github.com/camunda/camunda-bpm-platform/tree/master/engine/src/main/java/org/camunda/bpm/engine/impl/calendar
// https://docs.camunda.org/manual/7.9/reference/bpmn20/events/timer-events
Collection<TimerEventDefinition> getTimers(){
BpmnModelInstance model = (BpmnModelInstance)this
Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
return timerEventDefinitions
}
Map<String, Date> evaluateTimers(Date customCurrentTime = null) {
BpmnModelInstance model = (BpmnModelInstance) this
Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
Map<String, Date> timers = [:]
println customCurrentTime
if (customCurrentTime){
setCurrentTime(customCurrentTime)
}
println ClockUtil.getCurrentTime()
timerEventDefinitions.each { timer ->
Map<String, Date> timerEval = evaluateTimer(timer)
timers.putAll(timerEval)
}
if (customCurrentTime) {
resetCurrentTime()
}
return timers
}
private void setCurrentTime(Date customCurrentTime){
ClockUtil.setCurrentTime(customCurrentTime)
}
private void resetCurrentTime(){
ClockUtil.reset()
}
Map<String, Date> evaluateTimer(TimerEventDefinition timer, Date customCurrentTime = null){
Map<String,String> timerInfo = getTimerValue(timer)
String activityId = timer.getParentElement().getAttributeValue('id')
if (activityId == null){
throw new IOException('Could not get Activity Id of Timer Event Definition')
}
if (customCurrentTime){
setCurrentTime(customCurrentTime)
}
switch (timerInfo) {
case { it.type == 'date'}:
DueDateBusinessCalendar dueDateCalendar = new DueDateBusinessCalendar()
Date dueDate = dueDateCalendar.resolveDuedate(timerInfo.value)
if (customCurrentTime){
resetCurrentTime()
}
return [(activityId) : dueDate]
case {it.type == 'cycle'}:
CycleBusinessCalendar cycleBusinessCalendar = new CycleBusinessCalendar()
Date cycleDueDate = cycleBusinessCalendar.resolveDuedate(timerInfo.value)
if (customCurrentTime){
resetCurrentTime()
}
return [(activityId) : cycleDueDate]
case {it.type == 'duration'}:
DurationBusinessCalendar durationCalendar = new DurationBusinessCalendar()
Date durationDueDate = durationCalendar.resolveDuedate(timerInfo.value)
if (customCurrentTime){
resetCurrentTime()
}
return [(activityId) : durationDueDate]
default:
throw new IOException('Invalid Timer mapping found: must be of type: date or cycle or duration')
}
}
Map<String, String> getTimerValue(TimerEventDefinition timer) {
if (timer.getTimeDate() != null) {
return [('type'):'date',
('value'): timer.getTimeDate().getRawTextContent()]
} else if (timer.getTimeCycle() != null) {
return [('type'):'cycle',
('value'): timer.getTimeCycle().getRawTextContent()]
} else if (timer.getTimeDuration() != null) {
return [('type'):'duration',
('value'): timer.getTimeDuration().getRawTextContent()]
} else {
throw new IOException('Timer definition missing; Timer definition is required on all timers')
}
}
}
Here is a cleaner example of datatable usage to provide better expectations of testing:
...
class TimerTestSpec extends Specification implements bpmnTimers{
@Shared BpmnModelInstance model
@Shared SimpleDateFormat dateF = new SimpleDateFormat("yyyy MM dd - HH:mm")
def setupSpec(){
String path = 'bpmn/qa-test.bpmn'
InputStream bpmnFile = this.class.getResource(path.toString()).newInputStream()
model = Bpmn.readModelFromStream(bpmnFile).withTraits(bpmnTimers)
}
def 'Start Event Cycle Cron Test'(Date customStartTime, String expectedResultTime){
when: 'Given a Timer Start Event that we eval with a custom start date of #customStartTime'
String activityId = 'StartEvent_0ii048j'
TimerEventDefinition timerEvent = model.getTimerById(activityId)
Map<String,Date> result = evaluateTimer(timerEvent, customStartTime)
then: 'The timer should have a due date set to #execptedResultTime'
assert result.get(activityId).toString() == expectedResultTime
where:
customStartTime || expectedResultTime
dateF.parse('2011 01 01 - 00:00') || 'Sat Jan 01 01:00:00 EST 2011'
dateF.parse('2011 01 01 - 04:49') || 'Sat Jan 01 05:00:00 EST 2011'
dateF.parse('2011 01 01 - 05:01') || 'Sat Jan 01 10:00:00 EST 2011'
dateF.parse('2011 01 01 - 09:59') || 'Sat Jan 01 10:00:00 EST 2011'
dateF.parse('2011 01 01 - 14:30') || 'Sat Jan 01 15:00:00 EST 2011'
dateF.parse('2011 01 01 - 19:30') || 'Sat Jan 01 20:00:00 EST 2011'
dateF.parse('2011 01 01 - 21:00') || 'Tue Feb 01 01:00:00 EST 2011'
}
}
where traits are:
trait bpmnTimers{
// reference: https://github.com/camunda/camunda-bpm-platform/tree/master/engine/src/main/java/org/camunda/bpm/engine/impl/calendar
// https://docs.camunda.org/manual/7.9/reference/bpmn20/events/timer-events
Collection<TimerEventDefinition> getTimers(){
BpmnModelInstance model = (BpmnModelInstance)this
Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
return timerEventDefinitions
}
TimerEventDefinition getTimerById(String activityId){
BpmnModelInstance model = (BpmnModelInstance)this
TimerEventDefinition timerEventDefinition = model.getModelElementsByType(TimerEventDefinition.class).find {
it.getParentElement().getAttributeValue('id') == activityId
}
return timerEventDefinition
}
Map<String, Date> evaluateTimers(Date customCurrentTime = null) {
BpmnModelInstance model = (BpmnModelInstance) this
Collection<TimerEventDefinition> timerEventDefinitions = model.getModelElementsByType(TimerEventDefinition.class)
Map<String, Date> timers = [:]
println customCurrentTime
if (customCurrentTime){
setCurrentTime(customCurrentTime)
}
println ClockUtil.getCurrentTime()
timerEventDefinitions.each { timer ->
Map<String, Date> timerEval = evaluateTimer(timer)
timers.putAll(timerEval)
}
if (customCurrentTime) {
resetCurrentTime()
}
return timers
}
private void setCurrentTime(Date customCurrentTime){
ClockUtil.setCurrentTime(customCurrentTime)
}
private void resetCurrentTime(){
ClockUtil.reset()
}
Map<String, Date> evaluateTimer(TimerEventDefinition timer, Date customCurrentTime = null){
Map<String,String> timerInfo = getTimerValue(timer)
String activityId = timer.getParentElement().getAttributeValue('id')
if (activityId == null){
throw new IOException('Could not get Activity Id of Timer Event Definition')
}
if (customCurrentTime){
setCurrentTime(customCurrentTime)
}
switch (timerInfo) {
case { it.type == 'date'}:
DueDateBusinessCalendar dueDateCalendar = new DueDateBusinessCalendar()
Date dueDate = dueDateCalendar.resolveDuedate(timerInfo.value)
if (customCurrentTime){
resetCurrentTime()
}
return [(activityId) : dueDate]
case {it.type == 'cycle'}:
CycleBusinessCalendar cycleBusinessCalendar = new CycleBusinessCalendar()
Date cycleDueDate = cycleBusinessCalendar.resolveDuedate(timerInfo.value)
if (customCurrentTime){
resetCurrentTime()
}
return [(activityId) : cycleDueDate]
case {it.type == 'duration'}:
DurationBusinessCalendar durationCalendar = new DurationBusinessCalendar()
Date durationDueDate = durationCalendar.resolveDuedate(timerInfo.value)
if (customCurrentTime){
resetCurrentTime()
}
return [(activityId) : durationDueDate]
default:
throw new IOException('Invalid Timer mapping found: must be of type: date or cycle or duration')
}
}
Map<String, String> getTimerValue(TimerEventDefinition timer) {
if (timer.getTimeDate() != null) {
return [('type'):'date',
('value'): timer.getTimeDate().getRawTextContent()]
} else if (timer.getTimeCycle() != null) {
return [('type'):'cycle',
('value'): timer.getTimeCycle().getRawTextContent()]
} else if (timer.getTimeDuration() != null) {
return [('type'):'duration',
('value'): timer.getTimeDuration().getRawTextContent()]
} else {
throw new IOException('Timer definition missing; Timer definition is required on all timers')
}
}
}
Further explanation can be found here:
So imagine we have a Timer Start Event with a Cycle Definition of: 0 0 1,5,10,15,20 1 ? . This cron basically says: On the First of Every Month, at 01:00, 05:00, 10:00, 15:00, and 20:00, the cron should execute.
So in the where statement we test this using a “customStartTime” and a “expectedResultTime” data table. I have used two different styles of data comparison for demonstration purposes for ideas on how data could be entered (as valid dates, as strings, short dates and times, etc).
So the first input start time is 2011/01/91 at 00:00 and so based on the cron the first result should be a 1am Timer. Yada yada yada, and we arrive at the end of the data table were we have tested all of the previous scenarios, and now we are testing the change over to the next month with a start date of 2011/01/01 at 21:00, and thus past the last job which would have been at 20:00, and so the expected result time is Feb 1 at 01:00.
All assets successfully and we did not need to boot up the Camunda engine to test it out
When comparing as Strings the errors look something like:
result.get(activityId).toString() == expectedResultTime
| | | | | |
| | | | | Sat Jan 01 01:10:00 EST 2011
| | | | false
| | | | 1 difference (96% similarity)
| | | | Sat Jan 01 01:(0)0:00 EST 2011
| | | | Sat Jan 01 01:(1)0:00 EST 2011
| | | Sat Jan 01 01:00:00 EST 2011
| | StartEvent_0ii048j
| Sat Jan 01 01:00:00 EST 2011
[StartEvent_0ii048j:Sat Jan 01 01:00:00 EST 2011]
Hey!
been watching progress and wanted to get some insights into the work to date:
I have been working on https://github.com/camunda/camunda-bpm-process-test-coverage/issues/32 and I wanted to add the vPav style tests into the coverage testing:
Here is a working example that recreates your Redundant sequence flow test:
I decided to test out these reusable tests using Groovy Traits, so each "trait" / validation can be chosen to be added as seen fit by the testers.
see the
implements doesNotContainRedundantSequenceFlows
The trait looks like:
I also add the ability to allow redundant sequence flows in scenarios where the BPMN may warrant duplication. see the second param of the doesNotContainRedundantSequenceFlows() method.