Letzte Änderung am 30. November 2021 by Christoph Jüngling
Über Integration wurde in den letzten Jahren sehr viel geschrieben, und auch in diesem Artikel soll es genau darum gehen – allerdings gänzlich unpolitisch. Ich möchte nämlich zeigen, wie sich verschiedene Techniken, die ich in diesem Blog bereits behandelt habe, integrieren lassen, so dass gemeinsam etwas neues, besseres, schöneres entsteht. Dazu ist es erforderlich, dass alle beteiligten Komponenten im Sinne der Sache zusammenarbeiten. Und das mag man gerne nun doch als ein politisches Statement verstehen, denn nur so geht es.
In diesem Blog habe ich ja schon so einige Themen im Hinblick auf die Software-Entwicklung behandelt. Kein Wunder, das mache ich ja auch tagein tagaus. Dieser Beitrag wird nun einige davon zusammenführen, so dass (hoffentlich) ein Gesamtbild entsteht, wieso wir uns diese ganze Mühe überhaupt machen. Ich will ein Szenario entwerfen, wie es im Grunde typisch für den Entwickleralltag sein könnte – jedenfalls sollte es das, meine ich. Es geht um die Programmierung einer Software, wobei die konkrete Programmiersprache zunächst einmal bedeutungslos ist. Denn die hier geschilderten Verfahrensweisen lassen sich sicher bei jeder Sprache anwenden.
In diesem Artikel
Anforderungen
Nachdem wir (hoffentlich) die Anforderungen des Auftraggebers oder Anwenders sorgfältig notiert haben … Ach so, richtig, wie geht das? Eigentlich ist das recht einfach, wenn man mal ein wenig Verstand hinzu nimmt:
- Sammle alles, was du vom Auftraggeber gesagt oder geschrieben bekommst
- Formuliere es so kurz wie möglich, dabei aber so umfangreich wie nötig
- Begrenze jede Anforderung auf einen einzigen Wunsch (also nicht “das Programm soll a, b und c können”, das sind drei Anforderungen!)
- Jede Anforderung bekommt eine eindeutige Kennzeichnung (Nummer, Kurzbegriff, egal was, nur eindeutig)
Das wäre schon mal ein guter Anfang (dazu kann man natürlich noch viel mehr sagen, aber das würde jetzt den Rahmen sprengen). Dafür können wir z.B. ein Bugtracking-System (Bugzilla, Mantis, Jira, oder eine der zahlreichen Issue-Tracker, die in Github, Gitlab, Bitbucket etc. verfügbar sind) verwenden. Dieses kann dann später während der Einführungs- und Betriebsphase auch für die Erfassung der Bugs eingesetzt werden.
Quellcodeverwaltung
Code sollte nicht “einfach so” entstehen, sondern überlegt. Zu viel Planung ist nicht sinnvoll, aber zu wenig auch nicht. Jeder Fehler in dieser Phase rächt sich später doppelt und mehrfach. Das lässt sich zwar manchmal nicht vermeiden, denn wir sind alle Menschen, und Menschen machen Fehler. Eine gut gepflegte Quellcodeverwaltung kann uns dann aber dabei helfen, den Commit zu identifizieren, wo der Fehler hinein gekommen ist, halb- oder vollautomatisch (siehe dazu Finde den Übeltäter!). Wenn wir darauf achten, niemals zu viele und nicht verschiedene Änderungen in einem Commit zu erfassen, dann ist das Ergebnis eines bisect
-Laufs auch leicht zu lesen. Das Problem zu finden ist dann oft schon mehr als die halbe Lösung.
Die Umsetzung einer Anforderung im Code wird also in unserer Quellcodeverwaltung erfasst, wobei zu jedem Commit die Nummer der davon betroffenen Anforderung hinzugefügt wird. Je nach verwendetem Issue-Tracker mag das etwas anders aussehen, vielleicht #12345
(Bugnummer) oder ABC-12345
(Projektkürzel und Bugnummer). Wichtig ist nur, dass diese Referenz eindeutig vom restlichen Text der Commit-Message unterschieden werden kann. Dies kann man z.B. in der Logansicht der Tortoise-Produkte dann mittels einer Regular Expression hervorheben lassen und sogar gleich mit dem Anforderungssystem verlinken. Bequemer geht es kaum noch.
Python
Wir programmieren also fleißig vor uns hin, verwenden ordentlich unsere Quellcodeverwaltung und haken die Anforderungen immer schön ab wenn mal wieder eine erledigt wurde. Doch halt – woher wissen wir überhaupt, dass wir eine Anforderung wirklich “geschafft” haben? Das kommt jetzt auf das Level der Anforderung an. Ist es etwas, das der User sehen will (“der Bildschirmhintergrund soll zartrosa sein”), dann werden wir wohl oder übel den Auftraggeber oder sogar die User fragen müssen, was man dort von unserem Rosa hält – also ein “Akzeptanztest”.
Handelt des sich eher um eine technische Anforderung (“das Programm muss in der Lage sein, Excel-Files zu verarbeiten”), dann wird der oben genannte wohl bestenfalls feststellen können, dass keine Fehlermeldung kommt. Aber das reicht nicht – ein Fall für “Unit-Tests”.
(Unit-) Tests
Theoretisch könnte dieses Kapitel auch vor “Python” stehen, dann würden wir von Test-Driven-Devlopment (TDD) reden. Doch egal was zuerst kommt, sinnvollerweise brauchen wir beides: Test und Programm! Nun ist es zum Beispiel in Eclipse nicht weiter schwierig, alle Unit-Tests automatisch bei jeder Codeänderung (genau genommen “Speicherung”) auszuführen. Aber was nützt mir das, wenn das ganze auf einem Buildserver laufen soll?
Die Unit-Tests sind in meinem Projekten übrigens immer in einem eigenen Verzeichnis unittests
innerhalb des Projektes gespeichert, während der Code des Projektes selbst parallel dazu unter src
steht.
Integration
So weit, so gut. Die Bestandteile haben wir nun zusammen, jetzt geht es an die Integration. Dazu benötigen wir einen CI-Dienst. “CI” ist die Abkürzung für “continuous integration”, auf Deutsch eben genau der Titel dieses Beitrags. Ich will das am Beispiel von Jenkins zeigen, da ich diesen in einem Projekt verwende. So etwas geht aber sicher auch mit anderen Diensten. Dabei will ich auf die Details von Jenkins diesmal nicht eingehen, da das Prinzip auch leicht in bestehenden Jobs nachgerüstet werden kann.
Ich ziehe es vor, die eigentlichen Aufgaben in einer Windows-CMD-Datei im Projekt-Hauptordner zu speichern. Dadurch kann ich den Buildprozess auch lokal ausführen und testen. Die Datei (oder mehrere) werden dann wie der andere Code auch eingecheckt, wodurch auch Jenkins immer Zugriff darauf hat. In meinem Projekten verwende ich immer make.cmd
als Dateiname, aber grundsätzlich ist dieser natürlich beliebig.
Für die Ausführung der Unit-Tests auf der Kommandozeile lege ich daher ebenfalls eine solche Datei an, ich nenne sie runtests.cmd
. Wegen der oben erwähnten Verzeichnisstruktur wechseln wir zunächst (Zeile 1) vom Hauptverzeichnis des Projektes (das ist bei Jenkins so als Startverzeichnis eingestellt) in den Unterordner “unittests”, wozu wir den Windows-Befehl “pushd” (“push directory”) verwenden. Er korrespondiert mit “popd” (“pop directory”), der den ursprünglichen Zustand wieder herstellt (Zeile 6).
Der PYTHONPATH muss auf das Quellverzeichnis des Codes, der getestet werden soll, gesetzt und am Ende wieder entfernt werden (Zeilen 2 und 5).
Und letztlich steht in Zeile 3 der eigentliche Testaufruf. Zeile 4 dient dazu einen Fehler im Testrun zu entdecken (der Return-Code ist dann größer als 0) und den Batch mit diesem Errorlevel zu beenden. Wichtig ist dabei die Position des “if errorlevel”-Befehls, da jeder weitere Befehl u.U. den Errorlevel wieder ändert. Dabei wird zwar der Pythonpath und das Verzeichnis nicht zurückgesetzt, aber da der Jenkins-Lauf danach ohnehin beendet sein soll, stört mich das nicht weiter.
runtests.cmd:
pushd unittests
set PYTHONPATH=..\src
python -m unittest discover
if errorlevel 1 exit /b
set PYTHONPATH=
popd
Nun müssen wir nur noch in Jenkins diese beiden Batches ausführen. Das Buildverfahren in Jenkins heißt dabei “Windows Batch-Datei ausführen”.
call runtests.cmd && call make.cmd
Die beiden “kaufmännischen Unds” verknüpfen diese Befehle derart miteinander, dass der zweite Befehl nur bei erfolgreichem ersten ausgeführt wird. Folglich läuft “make” nur, wenn “runtests” erfolgreich war. War der Test hingegen fehlerhaft, wird der Batch mit dessen Fehlerstatus beendet, was Jenkins dazu bringt, die rote Lampe anzumachen. Ist alles gut (auch mit dem Make), wird die Lampe grün.
Tja, und das war’s schon. Fröhliche Integration!
Neueste Kommentare