Qualität von Workflows - Wie Sie Software-Tests effizienter machen

Dieser Artikel thematisiert den Stellenwert von Software-Tests zur Qualitätssicherung BPMN 2.0 konformer Camunda-Workflows. Dabei wird vor allem gezeigt wie Software-Tests und der Entwicklungsprozess durch Verwendung von Community Extensions/Plugins effizienter gestaltet werden können.

Egal ob smarte Heizungsanlage in den eigenen vier Wänden, automatisch aufpoppende Termine auf den grell flackernden Smartphone Bildschirmen oder einfach nur die Interaktion im favorisierten Computerspiel; alles Software, nein eher schon "Begleiter", die man nur ungerne im Alltag missen möchte. Diese "Begleiter" wären ohne qualitative Softwareentwicklung nicht denkbar! Doch was ist qualitative Software(entwicklung)? Wie kann Qualität erreicht werden? Qualitative Software zeichnet sich u. a. durch das Vorhandensein folgender Merkmale aus:

  • Wartbarkeit
  • Tests
  • (Aktuelle) Dokumentation

Alle obigen Punkte sind wichtig. Bei der Qualitätssicherung spielt jedoch besonders das Testen eine wichtige Rolle. Deshalb führt dieser Artikel am Beispiel von Camunda-Workflows in das Testen selbiger ein. Die Basis für die hier gezeigten Tests bildet das Camunda BPM Online Training. Darüber hinaus steht die Entwicklung effizienter Software-Tests durch den gezielten Einsatz von Plugins und Community Extensions im Mittelpunkt des Artikels. Folgende Plugins/Community Extensions werden im Artikel verwendet:

Der Artikel ist somit als Best-Practice Ansatz gedacht. Sind Sie interessiert wie Sie Software noch effizienter testen und somit die Softwareentwicklung  erleichtern können? Dann bleiben Sie dran! Als Testobjekt wird uns der folgende Camunda-Workflow, WorkflowTest, dienen:

Was macht der Workflow? Der Workflow nimmt eine textuelle Eingabe von einem Benutzer entgegen und prüft diese anhand bestimmter Kriterien auf Gültigkeit. Dabei gibt es folgende Möglichkeiten den Workflow abzuschließen:

  • Eingabe ist eine Zahl größer oder gleich 0 -> Erfolg
  • Eingabe ist eine Zahl kleiner 0 -> Fehler im Geschäftsprozess (es sind nur Zahlen größer oder gleich 0 erlaubt)
  • Eingabe ist keine Zahl oder Fehler technischer Natur (Netzwerkausfall, Speicherprobleme, ...)

Die Software, die unserem Workflow das Leben "einhaucht", befindet sich im Projekt WorkflowTest, dessen Struktur in folgender Abbildung dargestellt ist:

 

Folgender Quelltext (Klasse NumberValidationDelegate), den es  zu testen gilt, dient der Validierung der Eingabe:

// Validates user input against domain-specific constraints.
public class NumberValidationDelegate implements JavaDelegate {

   @Override
   public void execute(DelegateExecution arg0) throws Exception {
      String intake = (String) arg0.getVariable("number");
      int x = Integer.parseInt(intake);
      if (x <= 0) {
         throw new BpmnError("invalid_input");
      }
   }

}

Nun geht es ans Eingemachte! Zum Testen dienen uns die folgenden zwei Klassen:

  • WorkflowTest1
  • WorkflowTest2

WorkflowTest1 zeigt wie Camunda-Workflows getestet werden können. Anschließend wird in WorkflowTest2 das isolierte Testen einzelner Software-Bausteine mit Hilfe der Community Extension camunda-bpm-mockito unter die Lupe genommen.

WorkflowTest1

public class WorkflowTest1 {

   // process rule needs to be initiated once and can be reused..
   @ClassRule
   @Rule
   public static ProcessEngineRule rule =  TestCoverageProcessEngineRuleBuilder.create().build();
   
   @Before
   public void setup() {
      init(rule.getProcessEngine());
   }

   @Test
   @Deployment(resources="process.bpmn")
 _@RequiredHistoryLevel(ProcessEngineConfiguration.HISTORY_ACTIVITY)

   public void testWorkflow() {
      Map<String, Object> variables = new HashMap<String, Object>();
      // testing "happy" path..
      variables.put("number", "31");
      testPath(variables, "EndEventNumberCreated");
      // testing business error path..
      variables.put("number", "-1");
      testPath(variables, "EndEventBusinessErrorCaught");
      // testing technical error path..
      variables.put("number", "flower");
      testPath(variables, "EndEventTechnicalErrorCaught");
   }

   /*
    * Invokes a process of the given workflow using defined variables
    * and verifies whether an specific end event has been taken during  
    * execution.
    */
   public void testPath(Map<String, Object> variables, String endEventId) {
      // invoke process "NumberGeneration" via its key "NumberGeneration"
      // and check for success..
      ProcessEngine engine = rule.getProcessEngine();
      ProcessInstance instance = engine.getRuntimeService().
         startProcessInstanceByKey("NumberGeneration");
      assertNotNull(instance);
      // check existence of the user task "UserTaskEnterNumber"..
      List tasks = engine.getTaskService().createTaskQuery().list();
      Task userTaskEnterNumber = null;
      for (Task task : tasks) {
         if (task.getTaskDefinitionKey().equals("UserTaskEnterNumber")) {
            userTaskEnterNumber = task;
            break;
         }
      }
      assertNotNull(userTaskEnterNumber);
      // pass variables to task "UserTaskEnterNumber" and complete it..
      engine.getTaskService().
         complete(userTaskEnterNumber.getId(), variables);
      // check whether an given end event has passed during execution
      // => if yes, success!
      assertThat(instance).isStarted().isEnded().hasPassed(endEventId);
   }

}

Das Kernstück des obigen Quelltextes ist die Methode testPath. Sie durchläuft unter Angabe einer Variable (= Eingabe) den Workflow und prüft ob ein bestimmtes Ereignis (end event) erreicht worden ist. Ist dies der Fall, so wurde der Pfad positiv getestet. Es gibt in unserem Workflow drei solche Ereignisse bzw. verschiedene Arten des Verlaufes. Idealerweise sollte eine vollständige Abdeckung des zu testenden Quelltextes angestrebt werden. Diesen Zweck erfüllt die Methode testWorkflow. Sie ruft testPath für alle möglichen Konstellationen auf, woduch der gesamte Workflow abgedeckt wird.  Vorteilhaft erweist sich hier die Verwendung der Community Extension Camunda BPM Process Test Coverage. Sie erstellt automatisch Grafiken des zu testenden Workflows, die Aufschluss über (nicht) abgedeckte Pfade geben. Somit erhält man einen besseren Überblick des zu testenden Quelltextes, was Zeit und Mühe beim Testen reduzieren kann. Die folgende Abbildung wurde mit besagter Extension erstellt und  zeigt den Anteil des abgedeckten Quelltextes in unserem Workflow:

Für unseren Workflow sind obige Tests ausreichend. Was wäre, wenn die Validierung nicht direkt von NumberValidationDelegate ausgeführt, sondern delegiert werden würde? Beispielsweise könnte sich die Validierung in einer anderen Klasse befinden, die von NumberValidationDelegate verwendet wird. Das könnte dann beispielsweise wie folgt aussehen:

// Validates user input against domain-specific constraints.
public class NumberValidationDelegate implements JavaDelegate {

   private Validator validator;

   public bool execute(String number) throws Exception {
      bool result = validator.Validate(number);
      return result;
   }

}

Bei dem unter WorkflowTest1 angeführten Test würde NumberValidationDelegate mit Abhängigkeit zu der in der Schnittstelle Validator ausgelagerten Validierung erfolgen. Abhängigkeiten dieser Art sollten bei Unit-Tests vermieden werden, damit nur die zu testende Klasse (hier: NumberValidationDelegate) selbst ohne äußere Einflüsse (isoliert) getestet wird. Ansonsten können z. B. durch Validator ausgelöste Fehler zu Fehlern in NumberValidationDelegate führen, obwohl NumberValidationDelegate für sich korrekt funktioniert. Zum isolierten Testen von NumberValidationDelegate verwenden wir das Mocking-Framework camunda-bpm-mockito. Dabei wird die Validator-Schnittstelle "vorgetäuscht", um immer gleiches Verhalten im Test zu erhalten. Wie das geht zeigt folgender Quelltext:

WorkflowTest2
public class WorkflowTest2 {

   @Rule
   public final ExpectedException error = ExpectedException.none();

   @Test
   public void testNumberValidationDelegate() throws Exception {
      // prepare mocked validator..
      Validator mocked_validator = mock(Validator.class);
      when(mocked_validator.Validate("31")).thenReturn(true);
      when(mocked_validator.Validate("-1")).thenReturn(false);
      when(mocked_validator.Validate("test")).thenThrow(new Exception());
      // create test candidate..
      NumberValidationDelegate testCandidate = new NumberValidationDelegate();
      testCandidate.setValidator(mocked_validator);
      // testing "happy" path..
      assertTrue(testCandidate.execute("31"));
      // testing business error path..
      assertFalse(testCandidate.execute("-1"));
      // testing technical error path..
      try {
         testCandidate.execute("test");
      } catch (Exception technical_error) {
         // success
      }
   }

}

Dieser Artikel gab eine Einführung in das Testen von Camunda-Workflows. Dabei wurde gezeigt wie Software-Tests durch die sinnvolle und konsequente Verwendung von Extensions/Plugins effizienter gestaltet werden können. Effizientere Tests können sich in Gewinn von Zeit und einem besseren Überblick beim Testen/Entwickeln äußern. Dadurch kann die  Qualität von Software gesteigert  werden, was nicht nur den Entwickleralltag effizienter gestaltet ;)

Literatur

BSc. Christopher Kerth
Erstellt: 2017-04-21
von: BSc.  Christopher Kerth
Stichworte: 

Qualität, Software-Test, Mocking-Framework(s), Plugins, Camunda