Did it happen to you? Someone makes a minor change to the BPMN model, then, when the system is deployed the next time, the deployment fails because Camunda thinks that BPMN file is invalid. (Even though all the tests are green.)
There is a way to catch such errors earlier – in scope of automated tests rather than during deployment.
Why you should validate BPMN files in automated tests
When you deploy a BPMN file into a Camunda process engine, it performs a number of basic checks. If any errors are detected, the BPMN file is not deployed.
This means that in order to check the validity of a BPMN file you need to try to deploy it to Camunda. This has several disadvantages.
First of all, this validation approach requires a running Camunda engine. You can do it inside a JUnit test by starting Camunda there. However, these tests are usually slower than normal JUnit tests and if you have many BPMN files, running those tests can slow down the build. This, in turn, can encourage developers to turn off those tests.
Big Question
The question is: How can we validate BPMN files just like Camunda does during deployment without a running Camunda engine?
Where does Camunda validate BPMN files during deployment?
First, we need to download Camunda source code. After IntelliJ Idea finished indexing it, you can search for valid in that code base. Among the search results you will find the file BpmnDeployer. There, you can find the following piece of code:
@Override
protected List<ProcessDefinitionEntity> transformDefinitions(
DeploymentEntity deployment, ResourceEntity resource,
Properties properties) {
byte[] bytes = resource.getBytes();
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
BpmnParse bpmnParse = bpmnParser
.createParse()
.sourceInputStream(inputStream)
.deployment(deployment)
.name(resource.getName());
if (!deployment.isValidatingSchema()) {
bpmnParse.setSchemaResource(null);
}
bpmnParse.execute();
if (!properties.contains(JOB_DECLARATIONS_PROPERTY)) {
properties.set(JOB_DECLARATIONS_PROPERTY, new HashMap<String, List<JobDeclaration<?, ?>>>());
}
properties.get(JOB_DECLARATIONS_PROPERTY).putAll(bpmnParse.getJobDeclarations());
return bpmnParse.getProcessDefinitions();
}
The line bpmnParse.execute(); leads us to the class BpmnParse where we find another interesting piece of code:
@Override
public BpmnParse execute() {
super.execute(); // schema validation
try {
parseRootElement();
} catch (BpmnParseException e) {
addError(e);
} catch (Exception e) {
LOG.parsingFailure(e);
// ALL unexpected exceptions should bubble up since they are not handled
// accordingly by underlying parse-methods and the process can't be
// deployed
throw LOG.parsingProcessException(e);
} finally {
if (hasWarnings()) {
logWarnings();
}
if (hasErrors()) {
throwExceptionForErrors();
}
}
return this;
}
It seems that it throws an exception if errors are detected in a BPMN file.
Calling BpmnParse from inside a minimal JUnit test
Now we need to call BpmnParse in a JUnit test. With a little experimenting we came up with this piece of code:
private void validateFile(String fileName) throws IOException {
try (FileInputStream inputStream = FileUtils.openInputStream(new File(fileName))) {
final ExpressionManager testExpressionManager = new ExpressionManager();
ProcessEngineConfigurationImpl processEngineConfiguration = new ProcessEngineConfigurationImpl() {
@Override
protected Collection<? extends CommandInterceptor> getDefaultCommandInterceptorsTxRequired() {
return null;
}
@Override
protected Collection<? extends CommandInterceptor> getDefaultCommandInterceptorsTxRequiresNew() {
return null;
}
@Override
public ExpressionManager getExpressionManager() {
return testExpressionManager;
}
@Override
public FormTypes getFormTypes() {
final FormTypes formTypes = new FormTypes();
formTypes.addFormType(new BooleanFormType());
formTypes.addFormType(new StringFormType());
formTypes.addFormType(new LongFormType());
return formTypes;
}
};
Context.setProcessEngineConfiguration(processEngineConfiguration);
BpmnParseFactory bpmnParseFactory = new DefaultBpmnParseFactory();
BpmnParser bpmnParser = new BpmnParser(testExpressionManager, bpmnParseFactory);
BpmnParse bpmnParse = bpmnParser.createParse()
.sourceInputStream(inputStream)
.deployment(new DeploymentEntity())
.name(fileName);
bpmnParse.execute();
}
catch (final ParseException exception) {
exception.printStackTrace();
Assert.fail("BPMN is invalid, see log output for details");
}
}
It detects syntactic errors in BPMN diagrams just like Camunda does at deployment.
Then, you can do a basic syntax check on a BPMN file using a call like:
@Test
public void test1() throws IOException {
validateFile("src/test/resources/diagram_1.bpmn");
}
You can use this code to make sure that syntactic errors in your BPMN diagrams are detected as early as possible.
What do you think, would it make sense to enhance this code, for example, make sure that it detects activities without the async-before flag set?
Dmitrii Pisarenko
JIT Developer
Photo by Markus Winkler on Unsplash