Trotz der allgemeinen Erkenntnis, dass es wichtig ist, seine Software zu testen, und dass viele dies bereits automatisiert tun, findet sich auf den Weiten von Habr kein Rezept zur Konfiguration einer Verbindung dieser beliebten Produkte in diesem Bereich, wie (unsere Favoriten) GitLab und JUnit. Lassen Sie uns diese Lücke schließen!

Einleitung
Zunächst möchte ich den Kontext festlegen:
- Da alle unsere Anwendungen in Kubernetes laufen, werden wir den Testlauf in der entsprechenden Infrastruktur betrachten.
- Für den Aufbau und das Deployment verwenden wir (was im Sinne von Infrastrukturkomponenten auch automatisch bedeutet, dass Helm verwendet wird).
- Ich werde nicht ins Detail der Testkreation eingehen: In unserem Fall schreibt der Kunde die Tests selbst, und wir gewährleisten nur deren Ausführung (und das Vorhandensein der entsprechenden Berichte im Merge-Request).
Wie wird die allgemeine Abfolge der Schritte aussehen?
- Der Aufbau der Anwendung — die Beschreibung dieses Schrittes lassen wir aus.
- Deployment der Anwendung in einen separaten Namespace des Kubernetes-Clusters und Durchführung des Testens.
- Suche nach Artefakten und Parsing des JUnit-Berichts durch GitLab.
- Löschen des zuvor erstellten Namespace.
Nun — zur Umsetzung!
Konfiguration
GitLab CI
Lassen Sie uns mit dem Fragment beginnen .gitlab-ci.yaml, der das Deployment der Anwendung und das Starten von Tests beschreibt. Die Auflistung ist recht umfangreich, daher wurde sie umfangreich mit Kommentaren ergänzt:
Variablen:
# Wir deklarieren die Version von werf, die wir verwenden möchten
WERF_VERSION: "1.0 beta"
.base_deploy: &base_deploy
script:
# Namespace in K8s erstellen, falls nicht vorhanden
- kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# wir laden werf und deployen – weitere Informationen finden Sie in der Dokumentation
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#deploy-stage)
- type multiwerf && source <(multiwerf use ${WERF_VERSION})
- werf version
- type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
- werf deploy --stages-storage :local
--namespace ${CI_ENVIRONMENT_SLUG}
--set "global.commit_ref_slug=${CI_COMMIT_REF_SLUG:-''}"
# Übergabe der Variable `run_tests`
# sie wird im Rendern des Helm-Releases verwendet
--set "global.run_tests=${RUN_TESTS:-no}"
--set "global.env=${CI_ENVIRONMENT_SLUG}"
# Timeout ändern (es gibt lange Tests) und übergeben im Release
--set "global.ci_timeout=${CI_TIMEOUT:-900}"
--timeout ${CI_TIMEOUT:-900}
dependencies:
- Build
.test-base: &test-base
extends: .base_deploy
before_script:
# Erstellen eines Verzeichnisses für den zukünftigen Bericht, basierend auf $CI_COMMIT_REF_SLUG
- mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# Notlösung, da GitLab Artefakte in seinem build-dir haben möchte
- mkdir ./tests || true
- ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
after_script:
# Nach Abschluss der Tests entfernen wir das Release zusammen mit der Jobbeschreibung
# (und möglicherweise seiner Infrastruktur)
- type multiwerf && source <(multiwerf use ${WERF_VERSION})
- werf version
- type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
- werf dismiss --namespace ${CI_ENVIRONMENT_SLUG} --with-namespace
# wir erlauben Fehler, aber Sie können das anders gestalten
allow_failure: true
variables:
RUN_TESTS: 'yes'
# Wir setzen den Kontext in werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
WERF_KUBE_CONTEXT: 'admin@stage-cluster'
tags:
# Wir verwenden einen Runner mit dem Tag `werf-runner`
- werf-runner
artifacts:
# Artefakte müssen gesammelt werden, um im Pipeline sichtbar zu sein
# und heruntergeladen werden zu können – z.B. für eine gründliche Analyse
paths:
- ./tests/${CI_COMMIT_REF_SLUG}/*
# Artefakte, die älter als eine Woche sind, werden gelöscht
expire_in: 7 days
# wichtig: diese Zeilen steuern das Parsen des Berichts durch GitLab
reports:
junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml
# Zur Vereinfachung sind hier nur zwei Phasen dargestellt
# in der Realität gibt es mehr – mindestens wegen des Deployments
stages:
- build
- tests
build:
stage: build
script:
# Build – wieder nach der werf-Dokumentation
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#build-stage)
- type multiwerf && source <(multiwerf use ${WERF_VERSION})
- werf version
- type werf && source <(werf ci-env gitlab --tagging-strategy tag-or-branch --verbose)
- werf build-and-publish --stages-storage :local
tags:
- werf-runner
except:
- schedules
run tests:
<<: *test-base
environment:
# "Salz der Benennung" des Namespace
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
name: tests-${CI_COMMIT_REF_SLUG}
stage: tests
except:
- schedulesKubernetes
Jetzt im Verzeichnis .helm/templates erstellen wir eine YAML mit einem Job — tests-job.yaml — für die Ausführung von Tests und den erforderlichen Kubernetes-Ressourcen. Erläuterungen siehe nach dem Listing:
{{- if eq .Values.global.run_tests "yes" }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: tests-script
data:
tests.sh: |
echo "======================"
echo "${APP_NAME} TESTS"
echo "======================"
cd /app
npm run test:ci
cp report.xml /app/test_results/${CI_COMMIT_REF_SLUG}/
echo ""
echo ""
echo ""
chown -R 999:999 /app/test_results/${CI_COMMIT_REF_SLUG}
---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Chart.Name }}-test
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "2"
"werf/watch-logs": "true"
spec:
activeDeadlineSeconds: {{ .Values.global.ci_timeout }}
backoffLimit: 1
template:
metadata:
name: {{ .Chart.Name }}-test
spec:
containers:
- name: test
command: ['bash', '-c', '/app/tests.sh']
{{ tuple "application" . | include "werf_container_image" | indent 8 }}
env:
- name: env
value: {{ .Values.global.env }}
- name: CI_COMMIT_REF_SLUG
value: {{ .Values.global.commit_ref_slug }}
- name: APP_NAME
value: {{ .Chart.Name }}
{{ tuple "application" . | include "werf_container_env" | indent 8 }}
volumeMounts:
- mountPath: /app/test_results/
name: data
- mountPath: /app/tests.sh
name: tests-script
subPath: tests.sh
tolerations:
- key: dedicated
operator: Exists
- key: node-role.kubernetes.io/master
operator: Exists
restartPolicy: OnFailure
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ .Chart.Name }}-pvc
- name: tests-script
configMap:
name: tests-script
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Chart.Name }}-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Mi
storageClassName: {{ .Chart.Name }}-{{ .Values.global.commit_ref_slug }}
volumeName: {{ .Values.global.commit_ref_slug }}
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: {{ .Values.global.commit_ref_slug }}
spec:
accessModes:
- ReadWriteOnce
capacity:
storage: 10Mi
local:
path: /mnt/tests/
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- kube-master
persistentVolumeReclaimPolicy: Delete
storageClassName: {{ .Chart.Name }}-{{ .Values.global.commit_ref_slug }}
{{- end }} Welche Ressourcen sind in dieser Konfiguration beschrieben? Bei der Bereitstellung erstellen wir einen einzigartigen Namespace für das Projekt (dies ist auch in .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) und darin werden bereitgestellt:
- ConfigMap mit dem Testskript;
- Auftrag mit der Beschreibung des Pods und der angegebenen Direktive
command, die die Tests tatsächlich startet; - PV und PVC, die es ermöglichen, Testdaten zu speichern.
Beachten Sie die Eingangsbedingung mit wenn zu Beginn des Manifests — entsprechend müssen andere YAML-Dateien des Helm-Charts mit der Anwendung in eine umgekehrte Konstruktion gewickelt werden, damit sie bei Tests nicht bereitgestellt werden. Das heißt:
{{- if ne .Values.global.run_tests "yes" }}
---
ich andererseits yaml
{{- end }}Wenn die Tests jedoch eine bestimmte Infrastruktur benötigen (zum Beispiel Redis, RabbitMQ, Mongo, PostgreSQL…) — ihre YAML-Dateien können nicht deaktiviert werden. Stellen Sie sie auch in der Testumgebung bereit… natürlich angepasst nach Ihrem Ermessen.
Letzter Schliff
Da der Build und die Bereitstellung derzeit mit werf auf der Build-Server (mit gitlab-runner) stattfinden, und der Pod mit den Tests auf dem Master gestartet wird, muss ein Verzeichnis nur auf dem Master erstellt und an den Runner übergeben werden, /mnt/tests zum Beispiel über NFS. Ein ausgearbeitetes Beispiel mit Erklärungen finden Sie in derK8s-Dokumentation. .
Das Ergebnis wird sein:
user@kube-master:~$ cat /etc/exports | grep tests
/mnt/tests IP_gitlab-builder/32(rw,nohide,insecure,no_subtree_check,sync,all_squash,anonuid=999,anongid=998)
user@gitlab-runner:~$ cat /etc/fstab | grep tests
IP_kube-master:/mnt/tests /mnt/tests nfs4 _netdev,auto 0 0Niemand hindert daran, eine NFS-Freigabe direkt auf dem gitlab-runner zu erstellen und sie dann in die Pods zu mounten.
Hinweis
Vielleicht fragen Sie sich, warum man überhaupt die Erstellung eines Jobs komplizieren sollte, wenn man das Skript mit den Tests einfach direkt auf dem Shell-Runner ausführen kann? Die Antwort ist recht einfach...
Einige Tests erfordern den Zugriff auf die Infrastruktur (MongoDB, RabbitMQ, PostgreSQL usw.), um die korrekte Funktionsweise zu überprüfen. Wir machen die Tests einheitlich – mit diesem Ansatz wird es einfach, solche zusätzlichen Entitäten einzubeziehen. Darüber hinaus erhalten wir standardmäßig eine einheitliche Vorgehensweise beim Deployment (auch wenn es NFS und zusätzliches Mounten von Verzeichnissen verwendet).
Ergebnis
Was werden wir sehen, wenn wir die vorbereitete Konfiguration anwenden?
Im Merge-Request wird eine zusammenfassende Statistik der Tests angezeigt, die in seinem letzten Pipeline ausgeführt wurden:

Für jeden Fehler hier kann man klicken, um Details zu erhalten:

NB: Der aufmerksame Leser wird bemerken, dass wir eine NodeJS-Anwendung testen, während auf den Screenshots .NET zu sehen ist... Sehen Sie das nicht als Überraschung: Im Rahmen der Artikelvorbereitung gab es einfach keine Fehler beim Testen der ersten Anwendung, sondern wir haben sie in einer anderen gefunden.
Fazit
Wie man sieht, nichts kompliziertes!
Im Prinzip, wenn Sie bereits einen Shell-Bauer haben, der funktioniert, und Kubernetes nicht benötigen — wird es noch einfacher sein, das Testing daran anzuschließen, als hier beschrieben. Und in finden Sie Beispiele für Ruby, Go, Gradle, Maven und einige andere.
P.S.
Lesen Sie auch in unserem Blog:
- «»;
- «»;
- «».
Quelle: habr.com
