Despite the fact that everyone knows perfectly well that it is important and necessary to test your software, and many have been doing it automatically for a long time, in the vastness of Habr there was not a single recipe for setting up a bunch of such popular products in this niche as (we love) GitLab and JUnit . Let's fill this gap!

Introductory
Let me start with the context:
- Since all of our applications run on Kubernetes, running the tests on the appropriate infrastructure will be considered.
- To build and deploy we use (in terms of infrastructure components, this also automatically means that Helm is involved).
- I will not go into the details of directly creating tests: in our case, the client writes the tests himself, and we only ensure their launch (and the presence of the corresponding report in the merge request).
What will the general sequence of actions look like?
- Assembly of the application - we will omit the description of this stage.
- Deploy the application to a separate namespace of the Kubernetes cluster and start testing.
- Finding artifacts and parsing a JUnit report by GitLab.
- Removing the previously created namespace'a.
Now - to implementation!
Setting
GitLab CI
Let's start with a snippet .gitlab-ci.yaml, which describes the deployment of the application and the launch of tests. The listing turned out to be quite voluminous, therefore it is thoroughly supplemented with comments:
variables:
# ΠΎΠ±ΡΡΠ²Π»ΡΠ΅ΠΌ Π²Π΅ΡΡΠΈΡ werf, ΠΊΠΎΡΠΎΡΡΡ ΡΠΎΠ±ΠΈΡΠ°Π΅ΠΌΡΡ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡ
WERF_VERSION: "1.0 beta"
.base_deploy: &base_deploy
script:
# ΡΠΎΠ·Π΄Π°Π΅ΠΌ namespace Π² K8s, Π΅ΡΠ»ΠΈ Π΅Π³ΠΎ Π½Π΅Ρ
- kubectl --context="${WERF_KUBE_CONTEXT}" get ns ${CI_ENVIRONMENT_SLUG} || kubectl create ns ${CI_ENVIRONMENT_SLUG}
# Π·Π°Π³ΡΡΠΆΠ°Π΅ΠΌ werf ΠΈ Π΄Π΅ΠΏΠ»ΠΎΠΈΠΌ β ΠΏΠΎΠ΄ΡΠΎΠ±Π½Π΅Π΅ ΠΎΠ± ΡΡΠΎΠΌ ΡΠΌ. Π² Π΄ΠΎΠΊΡΠΌΠ΅Π½ΡΠ°ΡΠΈΠΈ
# (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:-''}"
# ΠΏΠ΅ΡΠ΅Π΄Π°Π΅ΠΌ ΠΏΠ΅ΡΠ΅ΠΌΠ΅Π½Π½ΡΡ `run_tests`
# ΠΎΠ½Π° Π±ΡΠ΄Π΅Ρ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°ΡΡΡΡ Π² ΡΠ΅Π½Π΄Π΅ΡΠ΅ Helm-ΡΠ΅Π»ΠΈΠ·Π°
--set "global.run_tests=${RUN_TESTS:-no}"
--set "global.env=${CI_ENVIRONMENT_SLUG}"
# ΠΈΠ·ΠΌΠ΅Π½ΡΠ΅ΠΌ timeout (Π±ΡΠ²Π°ΡΡ Π΄ΠΎΠ»Π³ΠΈΠ΅ ΡΠ΅ΡΡΡ) ΠΈ ΠΏΠ΅ΡΠ΅Π΄Π°Π΅ΠΌ Π΅Π³ΠΎ Π² ΡΠ΅Π»ΠΈΠ·
--set "global.ci_timeout=${CI_TIMEOUT:-900}"
--timeout ${CI_TIMEOUT:-900}
dependencies:
- Build
.test-base: &test-base
extends: .base_deploy
before_script:
# ΡΠΎΠ·Π΄Π°Π΅ΠΌ Π΄ΠΈΡΠ΅ΠΊΡΠΎΡΠΈΡ Π΄Π»Ρ Π±ΡΠ΄ΡΡΠ΅Π³ΠΎ ΠΎΡΡΠ΅ΡΠ°, ΠΈΡΡ
ΠΎΠ΄Ρ ΠΈΠ· $CI_COMMIT_REF_SLUG
- mkdir /mnt/tests/${CI_COMMIT_REF_SLUG} || true
# Π²ΡΠ½ΡΠΆΠ΄Π΅Π½Π½ΡΠΉ ΠΊΠΎΡΡΡΠ»Ρ, Ρ.ΠΊ. GitLab Ρ
ΠΎΡΠ΅Ρ ΠΏΠΎΠ»ΡΡΠΈΡΡ Π°ΡΡΠ΅ΡΠ°ΠΊΡΡ Π² ΡΠ²ΠΎΠ΅ΠΌ build-dirβΠ΅
- mkdir ./tests || true
- ln -s /mnt/tests/${CI_COMMIT_REF_SLUG} ./tests/${CI_COMMIT_REF_SLUG}
after_script:
# ΠΏΠΎΡΠ»Π΅ ΠΎΠΊΠΎΠ½ΡΠ°Π½ΠΈΡ ΡΠ΅ΡΡΠΎΠ² ΡΠ΄Π°Π»ΡΠ΅ΠΌ ΡΠ΅Π»ΠΈΠ· Π²ΠΌΠ΅ΡΡΠ΅ Ρ JobβΠΎΠΌ
# (ΠΈ, Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎ, Π΅Π³ΠΎ ΠΈΠ½ΡΡΠ°ΡΡΡΡΠΊΡΡΡΠΎΠΉ)
- 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
# ΠΌΡ ΡΠ°Π·ΡΠ΅ΡΠ°Π΅ΠΌ ΠΏΠ°Π΄Π΅Π½ΠΈΡ, Π½ΠΎ Π²Ρ ΠΌΠΎΠΆΠ΅ΡΠ΅ ΡΠ΄Π΅Π»Π°ΡΡ ΠΈΠ½Π°ΡΠ΅
allow_failure: true
variables:
RUN_TESTS: 'yes'
# Π·Π°Π΄Π°Π΅ΠΌ ΠΊΠΎΠ½ΡΠ΅ΠΊΡΡ Π² werf
# (https://werf.io/how_to/gitlab_ci_cd_integration.html#infrastructure)
WERF_KUBE_CONTEXT: 'admin@stage-cluster'
tags:
# ΠΈΡΠΏΠΎΠ»ΡΠ·ΡΠ΅ΠΌ ΡΠ°Π½Π½Π΅Ρ Ρ ΡΠ΅Π³ΠΎΠΌ `werf-runner`
- werf-runner
artifacts:
# ΡΡΠ΅Π±ΡΠ΅ΡΡΡ ΡΠΎΠ±ΡΠ°ΡΡ Π°ΡΡΠ΅ΡΠ°ΠΊΡ Π΄Π»Ρ ΡΠΎΠ³ΠΎ, ΡΡΠΎΠ±Ρ Π΅Π³ΠΎ ΠΌΠΎΠΆΠ½ΠΎ Π±ΡΠ»ΠΎ ΡΠ²ΠΈΠ΄Π΅ΡΡ
# Π² ΠΏΠ°ΠΉΠΏΠ»Π°ΠΉΠ½Π΅ ΠΈ ΡΠΊΠ°ΡΠ°ΡΡ β Π½Π°ΠΏΡΠΈΠΌΠ΅Ρ, Π΄Π»Ρ Π±ΠΎΠ»Π΅Π΅ Π²Π΄ΡΠΌΡΠΈΠ²ΠΎΠ³ΠΎ ΠΈΠ·ΡΡΠ΅Π½ΠΈΡ
paths:
- ./tests/${CI_COMMIT_REF_SLUG}/*
# Π°ΡΡΠ΅ΡΠ°ΠΊΡΡ ΡΡΠ°ΡΡΠ΅ Π½Π΅Π΄Π΅Π»ΠΈ Π±ΡΠ΄ΡΡ ΡΠ΄Π°Π»Π΅Π½Ρ
expire_in: 7 day
# Π²Π°ΠΆΠ½ΠΎ: ΡΡΠΈ ΡΡΡΠΎΠΊΠΈ ΠΎΡΠ²Π΅ΡΠ°ΡΡ Π·Π° ΠΏΠ°ΡΡΠΈΠ½Π³ ΠΎΡΡΠ΅ΡΠ° GitLabβΠΎΠΌ
reports:
junit: ./tests/${CI_COMMIT_REF_SLUG}/report.xml
# Π΄Π»Ρ ΡΠΏΡΠΎΡΠ΅Π½ΠΈΡ Π·Π΄Π΅ΡΡ ΠΏΠΎΠΊΠ°Π·Π°Π½Ρ Π²ΡΠ΅Π³ΠΎ Π΄Π²Π΅ ΡΡΠ°Π΄ΠΈΠΈ
# Π² ΡΠ΅Π°Π»ΡΠ½ΠΎΡΡΠΈ ΠΆΠ΅ Ρ Π²Π°Ρ ΠΈΡ
Π±ΡΠ΄Π΅Ρ Π±ΠΎΠ»ΡΡΠ΅ β ΠΊΠ°ΠΊ ΠΌΠΈΠ½ΠΈΠΌΡΠΌ ΠΈΠ·-Π·Π° Π΄Π΅ΠΏΠ»ΠΎΡ
stages:
- build
- tests
build:
stage: build
script:
# ΡΠ±ΠΎΡΠΊΠ° β ΡΠ½ΠΎΠ²Π° ΠΏΠΎ Π΄ΠΎΠΊΡΠΌΠ΅Π½ΡΠ°ΡΠΈΠΈ ΠΏΠΎ 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:
# "ΡΠ°ΠΌΠ° ΡΠΎΠ»Ρ" ΠΈΠΌΠ΅Π½ΠΎΠ²Π°Π½ΠΈΡ namespaceβΠ°
# (https://docs.gitlab.com/ce/ci/variables/predefined_variables.html)
name: tests-${CI_COMMIT_REF_SLUG}
stage: tests
except:
- schedules
Kubernetes
Now in the directory .helm/templates create YAML with Job - tests-job.yaml - to run tests and the Kubernetes resources it needs. See explanation after 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 }}
What are the resources described in this configuration? When deploying, we create a unique namespace for the project (this is also indicated in .gitlab-ci.yaml tests-${CI_COMMIT_REF_SLUG}) and roll it into it:
- ConfigMap with test script;
- Jobs with pod description and specified directive
command, which just runs the tests; - PV and PVC, which allows you to store test data.
Pay attention to the introductory condition with if at the beginning of the manifest - accordingly, other YAML files of the Helm chart with the application must be wrapped in inverse construction so that they do not deploy during testing. That is:
{{- if ne .Values.global.run_tests "yes" }}
---
Ρ Π΄ΡΡΠ³ΠΎΠΉ ΡΠΌΠ»ΠΈΠΊ
{{- end }}
However, if tests require some infrastructure (for example, Redis, RabbitMQ, Mongo, PostgreSQL ...) - their YAMLs can be not turn off. Deploy them in a test environment ... of course, tweaking as you see fit.
Final touch
Because build and deploy with werf works so far only on the build server (with gitlab-runner), and the pod with tests is launched on the master, you need to create a directory /mnt/tests on the master and give it to the runner, e.g. via NFS. An expanded example with explanations can be found in .
The result will be:
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 0
Nobody forbids to make an NFS-share right on the gitlab-runner, and then mount it in pods.
Note
You may be asking why complicate things at all by creating a Job when you can just run the script with the tests right on the shell runner? The answer is rather trivial...
Some tests require access to the infrastructure (MongoDB, RabbitMQ, PostgreSQL, etc.) to check the correctness of working with them. We make testing unified - with this approach, it becomes easy to include such additional entities. In addition to this, we get standard deployment approach (even if using NFS, additional mounting of directories).
Experience the Power of Effective Results
What will we see when we apply the prepared configuration?
The merge request will show summary statistics for tests run in its latest pipeline:

Each error can be clicked here for details:

NB: An attentive reader will notice that we are testing a NodeJS application, and in the screenshots - .NET ... Do not be surprised: just as part of the preparation of the article, there were no errors in testing the first application, but they were found in another.
Conclusion
As you can see, nothing complicated!
In principle, if you already have a shell builder and it works, but you donβt need Kubernetes, attaching testing to it will be an even easier task than described here. And in you'll find examples for Ruby, Go, Gradle, Maven, and more.
P.S.
Read also on our blog:
- Β«";
- Β«";
- «».
Source: habr.com
