Deși toată lumea știe că este important și necesar să ne testăm software-ul, iar mulți fac acest lucru automat de ceva vreme, pe Habr nu am găsit nicio rețetă pentru configurarea unei combinări dintre produsele populare din această nișă, cum ar fi (preferatul nostru) GitLab și JUnit. Să umplem această lacună!

Introducere
Pentru început, voi delimita contextul:
- Având în vedere că toate aplicațiile noastre funcționează în Kubernetes, vom discuta despre rularea testelor în infrastructura corespunzătoare.
- Pentru construirea și implementarea, folosim (în sensul componentelor de infrastructură, aceasta înseamnă de asemenea că este folosit Helm).
- Nu voi intra în detalii despre crearea efectivă a testelor: în cazul nostru, clientul scrie testele el însuși, iar noi ne ocupăm doar de rularea lor (și de generarea raportului corespunzător în merge request).
Care va fi secvența generală a pașilor?
- Construirea aplicației — vom omite descrierea acestui pas.
- Implementarea aplicației într-un namespace separat al clusterei Kubernetes și rularea testării.
- Căutarea artefactelor și parsarea raportului JUnit de către GitLab.
- Ștergerea namespace-ului creat anterior.
Acum — să trecem la implementare!
Configurare
GitLab CI
Să începem cu fragmentul .gitlab-ci.yaml, care descrie implementarea aplicației și rularea testelor. Listingul a fost destul de voluminos, așa că a fost completat cu comentarii suplimentare:
variabile:
# declarăm versiunea werf pe care intenționăm să o utilizăm
WERF_VERSION: "1.0 beta"
.base_deploy: &base_deploy
script:
# creăm namespace în K8s, dacă nu există
- kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# încărcăm werf și implementăm - mai multe detalii puteți găsi în documentație
# (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:-''}"
# transmitem variabila `run_tests`
# aceasta va fi utilizată în renderizarea Helm-ului
--set "global.run_tests=${RUN_TESTS:-no}"
--set "global.env=${CI_ENVIRONMENT_SLUG}"
# modificăm timeout-ul (unele teste pot dura mult) și îl transmitem în release
--set "global.ci_timeout=${CI_TIMEOUT:-900}"
--timeout ${CI_TIMEOUT:-900}
dependencies:
- Build
.test-base: &test-base
extends: .base_deploy
before_script:
# creăm un director pentru viitorul raport, bazat pe $CI_COMMIT_REF_SLUG
- mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# un workaround necesar, deoarece GitLab vrea să primească artefactele în build-dir-ul său
- mkdir ./tests || true
- ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
after_script:
# după terminarea testelor, ștergem release-ul împreună cu Job-ul
# (și, poate, infrastructura acestuia)
- 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
# permitem eșecurile, dar puteți proceda altfel
allow_failure: true
variables:
RUN_TESTS: 'yes'
# stabilim contextul în werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
WERF_KUBE_CONTEXT: 'admin@stage-cluster'
tags:
# folosim un runner cu eticheta `werf-runner`
- werf-runner
artifacts:
# este necesar să generăm un artefact pentru a putea fi vizibil
# în pipeline și descărcat - de exemplu, pentru o analiză mai amănunțită
paths:
- ./tests/${CI_COMMIT_REF_SLUG}/*
# artefactele mai vechi de o săptămână vor fi șterse
expire_in: 7 zile
# important: aceste linii sunt responsabile pentru parsarea raportului de către GitLab
reports:
junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml
# pentru simplificare, aici sunt prezentate doar două etape
# în realitate, veți avea mai multe - cel puțin din cauza implementării
stages:
- build
- tests
build:
stage: build
script:
# construcția - iarăși conform documentației werf
# (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:
# "sarea esențială" a denumirii namespace-ului
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
name: tests-${CI_COMMIT_REF_SLUG}
stage: tests
except:
- schedulesKubernetes
Acum în directorul .helm/templates vom crea un YAML cu Job-ul — tests-job.yaml — pentru a rula testele și resursele necesare Kubernetes. Explicațiile sunt după listare:
{{- 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 }} Ce resurse sunt descrise în această configurație? La desfășurare, creăm un namespace unic pentru proiect (acesta este specificat încă în .gitlab-ci.yaml — tests-${CI_COMMIT_REF_SLUG}) și îl implementăm în:
- ConfigMap cu scriptul testului;
- Job cu descrierea pod-ului și directiva specificată
command, care lansează testele; - PV și PVC, care permit stocarea datelor testelor.
Atenție la condiția de introducere de la if în partea de sus a manifestului — prin urmare, celelalte fișiere YAML ale chart-ului Helm cu aplicația trebuie să fie învelite în o structură inversă , astfel încât să nu fie desfășurate în timpul testării. Așadar:
{{- if ne .Values.global.run_tests "yes" }}
---
un alt yaml
{{- end }}Totuși, dacă testele cer o anumită infrastructură (de exemplu, Redis, RabbitMQ, Mongo, PostgreSQL…) — fișierele lor YAML pot nu fi dezactivate. Extindeți-le și în mediu de testare… desigur, modificându-le după bunul plac.
Ultimul retuș
Deoarece compilarea și implementarea folosind werf funcționează deocamdată doar pe serverul de compilare (cu gitlab-runner), iar podul cu teste se lansează pe master, va fi necesar să creați un director /mnt/tests pe master și să-l oferiți runner-ului, de exemplu, prin NFS. Un exemplu detaliat cu explicații poate fi găsit în .
Rezultatul va fi:
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 0Nimeni nu interzice să faceți un share NFS direct pe gitlab-runner, după care să-l montați în poduri.
Notă
Probabil vă întrebați de ce complicăm totul creând un Job, când putem pur și simplu să lansăm scriptul cu teste direct pe shell-runner? Răspunsul este destul de trivial…
Unele teste necesită acces la infrastructură (MongoDB, RabbitMQ, PostgreSQL etc.) pentru a verifica corectitudinea operațiunilor cu acestea. Facem testarea unificată — cu această abordare, includerea entităților suplimentare devine ușoară. În plus, obținem standard o abordare în implementare (chiar dacă folosim NFS, montând directoare suplimentare).
Rezultatul
Ce vom vedea când aplicăm configurația pregătită?
În merge request va fi afișată o statistică sumară pentru testele rulate în ultimul său pipeline:

Pentru fiecare eroare, aici se poate da clic pentru a obține detalii:

NB: Cititorul atent va observa că testăm aplicația NodeJS, iar în capturile de ecran — .NET… Nu vă mirați: doar în cadrul pregătirii articolului nu au fost găsite erori în testarea primei aplicații, dar au fost găsite în cealaltă.
Concluzie
După cum se vede, nimic complicat!
În principiu, dacă aveți deja un builder shell și funcționează, iar Kubernetes nu vă este necesar — atașarea testării la acesta va fi o sarcină și mai simplă decât cea descrisă aici. Și în veți găsi exemple pentru Ruby, Go, Gradle, Maven și câteva altele.
P.S.
Citiți și în blogul nostru:
- «»;
- «»;
- «».
Sursa: habr.com
