@bahmutov
@richyflowers
@bahmutov
@richyflowers
API
Kafka
Label
Capabilities
Simple: run web application and Cypress locally
Run Web application
Run Cypress
Host machine (Mac, Windows, Linux)
Simple: run web application in Docker container and Cypress locally
Run Web application
Run Cypress
Host machine (Mac, Windows, Linux)
Docker container
Medium: run web application in Docker container and Cypress in another Docker container
Run Web application
Run Cypress
Host machine (Mac, Windows, Linux)
Docker container
Docker container
version: '3'
services:
web:
image: apache
build: ./webapp
container_name: apache
restart: always
# we can see the server running at "localhost:8080"
ports:
- "8080:80"
e2e:
image: cypress
build: ./e2e
container_name: cypress
depends_on:
- web
# note: inside e2e container, the network allows accessing
# "web" host under name "web"
# so "curl http://web" would return whatever the webserver
# in the "web" container is cooking
# see https://docs.docker.com/compose/networking/
environment:
- CYPRESS_baseUrl=http://web
command: npx cypress run
# mount the host directory e2e/cypress and the file e2e/cypress.json as
# volumes within the container
# this means that:
# 1. anything that Cypress writes to these folders (e.g., screenshots,
# videos) appears also on the Docker host's filesystem
# 2. any change that the developer applies to Cypress files on the host
# machine immediately takes effect within the e2e container (no docker
# rebuild required).
volumes:
- ./e2e/cypress:/app/cypress
- ./e2e/cypress.json:/app/cypress.json
docker-compose.yml
Medium: Cypress in Docker container and web application anywhere
Run Web application
Run Cypress
Host machine (Mac, Windows, Linux)
Docker container
services:
zookeeper:
image: wurstmeister/zookeeper
...
kafka:
image: wurstmeister/kafka
...
dhl-cap:
image: dhlparcel/dhl-parcel-capability-service:${DOCKER_COMPOSE_CAP_VERSION:-latest}
...
dhl-lab:
image: dhlparcel/dhl-parcel-label-service:${DOCKER_COMPOSE_LAB_VERSION:-latest}
...
dhl-api:
image: dhlparcel/dhl-parcel-api:${DOCKER_COMPOSE_API_VERSION:-latest}
...
... all sorts of micro services ...
dhl-load-data:
image: dhlparcel/dhl-parcel-build-postgres:latest
...
services:
dhl-e2e-cypress:
image: dhlparcel/dhl-parcel-cypress:4.2.0
ipc: host
command: "sh -c '/var/mdp/scripts/compose/run-cypress.sh'"
volumes:
- ./:/var/mdp:rw
environment:
- CI_BUILD_ID=${CI_BUILD_ID}
- KEY=${MY_CYPRESS_KEY}
links:
- dhl-api:localhost.dhlparcel.nl
- dhl-api:localhost.dhlparcel.be
- dhl-api:localhost.dhlparcel.pt
- dhl-api:localhost.dhlparcel.es
- dhl-api:localhost.dhlparcel.eu
- dhl-api:localhost.dhlparcel.ch
- dhl-api:localhost.dhlparcel.se
- dhl-admin-api:localhost.admin.dhlparcel.nl
depends_on:
- dhl-load-data
#!/usr/bin/env bash
while ! timeout 1 bash -c "echo > /dev/tcp/dhl-api/5000" &> /dev/null; do
echo "Waiting dhl-api to launch on 5000..."
sleep 1
done
echo "Dhl-api launched"
cd /var/mdp/${PROJECT}/e2e_cypress && /home/node/node_modules/.bin/cypress run --record --key ${KEY}
def composeBuild = "-f docker-compose.yml -f docker-compose.jenkins.yml --project-name ${projectBuildNr}"
try {
stage("E2E") {
sh "docker-compose ${composeBuild} run --rm --no-deps dhl-e2e-cypress"
}
} finally {
sh "docker-compose ${composeBuild} logs --tail='all'"
junit "**/e2e_cypress/test-results.xml"
}
docker-compose run --help
--no-deps Don't start linked services.
--rm Remove container after run. Ignored in detached mode.
Start docker-compose
(2 React apps, 2 Angular apps)
~ 6 min
~ 2 min
~ 3 min
~ 1 min
Slow and you had to wait for it twice:
Once for your pull request, once for the master build
stage('Init e2e') {
sh "docker-compose ${composeBuild} up dhl-load-data"
}
parallel(
"B2X-1": {
stage("E2E - B2X - 1") {
sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-b2x-suite-1"
}
},
"B2X-2": {
stage("E2E - B2X - 2") {
sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-b2x-suite-2"
}
},
"C2C": {
stage("E2E - C2C") {
sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-c2c"
}
},
"Admin": {
stage("E2E - Admin") {
sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-admin"
}
},
"PWA": {
stage("E2E - PWA") {
sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-pwa"
}
},
)
dhl-e2e-b2x-suite-1:
image: dhlparcel/dhl-parcel-cypress:4.2.0
ipc: host
command: "sh -c '/var/mdp/scripts/compose/run-cypress.sh --spec='cypress/integration/suite-1-*"
volumes:
- ./:/var/mdp:rw
environment:
- PROJECT=business-site
links:
- dhl-api:localhost.dhlparcel.nl
- dhl-api:localhost.dhlparcel.be
- dhl-api:localhost.dhlparcel.pt
- dhl-api:localhost.dhlparcel.es
- dhl-api:localhost.dhlparcel.eu
- dhl-api:localhost.dhlparcel.ch
- mailcatcher:mailcatcher
depends_on:
- dhl-load-data
stage('Init e2e') {
sh "docker-compose ${composeBuild} up dhl-load-data"
}
parallel(
"B2X-1-2": {
stage("E2E - B2X - 1") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-b2x-suite-1" }
},
"B2X-1-1": {
...
},
"B2X-1-3": {
...
},
"B2X-1-4": {
...
},
"B2X-2-1": {
stage("E2E - B2X - 1") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-b2x-suite-2" }
},
"B2X-2-2": {
...
},
"B2X-2-3": {
...
},
"C2C": {
stage("E2E - C2C") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-c2c" }
},
"C2C": {
...
},
"Admin": {
...
},
"PWA-1": {
stage("E2E - PWA") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-pwa" }
},
"PWA-2": {
...
},
"PWA-3": {
...
},
"PWA-4": {
...
},
)
parallel(
"B2X-1": {
stage("E2E - B2X - 1") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-b2x-suite" }
},
"B2X-2": {
...
},
"B2X-3": {
...
},
"B2X-4": {
...
},
"B2X-5": {
...
},
"B2X-6": {
...
},
"B2X-7": {
...
},
"C2C": {
stage("E2E - C2C") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-c2c" }
},
"C2C": {
...
},
"Admin": {
...
},
"PWA-1": {
stage("E2E - PWA") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-pwa" }
},
"PWA-2": {
...
},
"PWA-3": {
...
},
"PWA-4": {
...
},
)
C2C / Admin end their run
PWA
4
2
1
Time
Load
4
2
1
Time
Load
Wasted CPU
while ! timeout 1 bash -c "echo > /dev/tcp/dhl-api/5000" &> /dev/null; do
echo "Waiting dhl-api to launch on 5000..."
sleep 1
done
echo "Dhl-api launched"
cd /var/mdp/business-site/e2e_cypress && /home/node/node_modules/.bin/cypress run --record --parallel --key ${B2X_KEY} --ci-build-id ${CI_BUILD_ID}
cd /var/mdp/pwa-site/e2e_cypress && /home/node/node_modules/.bin/cypress run --record --parallel --key ${PWA_KEY} --ci-build-id ${CI_BUILD_ID}
cd /var/mdp/admin-site/e2e_cypress && /home/node/node_modules/.bin/cypress run --record --parallel --key ${ADMIN_KEY} --ci-build-id ${CI_BUILD_ID}
cd /var/mdp/site/e2e_cypress && /home/node/node_modules/.bin/cypress run --record --parallel --key ${C2C_KEY} --ci-build-id ${CI_BUILD_ID}
run-cypress.sh
B2X
B2X
B2X
PWA
PWA
C2C
...
B2X
B2X
PWA
PWA
C2C
C2C
...
B2X
B2X
B2X
PWA
PWA
C2C
...
B2X
B2X
PWA
PWA
C2C
Admin
...
B2X
B2X
B2X
B2X
C2C
C2C
...
stage('Init e2e') {
sh "docker-compose ${composeBuild} up dhl-load-data"
}
parallel(
"E2E-1": {
stage("E2E - 1") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-cypress" }
},
"E2E-2": {
stage("E2E - 2") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-cypress" }
},
"E2E-3": {
stage("E2E - 3") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-cypress" }
},
"E2E-4": {
stage("E2E - 4") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-cypress" }
},
"E2E-5": {
stage("E2E - 5") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-cypress" }
},
"E2E-6": {
stage("E2E - 6") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-cypress" }
},
"E2E-7": {
stage("E2E - 7") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-cypress" }
},
"E2E-8": {
stage("E2E - 8") { sh "docker-compose ${composeBuild} ${runOnly} dhl-e2e-cypress" }
},
)
Time
Load
init step needed before run, risk of running twice
quite verbose in syntax
stage("Run E2E") {
sh "docker-compose ${composeBuild} up --scale dhl-e2e-cypress=8 dhl-e2e-cypress"
}
We done?
Jenkins
Jenkins only looked at the first exit code of the first container that stopped
First container ended successfully?
Build green, carry on
while ! timeout 1 bash -c "echo > /dev/tcp/dhl-api/5000" &> /dev/null; do
echo "Waiting dhl-api to launch on 5000..."
sleep 1
done
echo "Dhl-api launched"
err=0
trap '(( err += $? ))' ERR
cd /var/mdp/business-site/e2e_cypress && /home/node/node_modules/.bin/cypress run --record --parallel --key ${B2X_KEY} --ci-build-id ${CI_BUILD_ID}
cd /var/mdp/pwa-site/e2e_cypress && /home/node/node_modules/.bin/cypress run --record --parallel --key ${PWA_KEY} --ci-build-id ${CI_BUILD_ID}
cd /var/mdp/admin-site/e2e_cypress && /home/node/node_modules/.bin/cypress run --record --parallel --key ${ADMIN_KEY} --ci-build-id ${CI_BUILD_ID}
cd /var/mdp/site/e2e_cypress && /home/node/node_modules/.bin/cypress run --record --parallel --key ${C2C_KEY} --ci-build-id ${CI_BUILD_ID}
exit "$err"
run-cypress.sh
err=0
trap '(( err += $? ))' ERR
...
exit err
trap defines and activates handlers to be run when the shell receives signals or other special conditions.
"Run E2E": {
stage("Run E2E") {
sh "docker-compose ${composeBuild} up --scale dhl-e2e-cypress=8 dhl-e2e-cypress"
sh "docker-compose ${composeBuild} ps"
CYPRESS_ERROR_CODE = sh (
script: "docker-compose ${composeBuild} ps -q
| xargs docker inspect -f '{{ .State.ExitCode }}'
| grep -v '^0'
| wc -l
| tr -d ' '",
returnStdout: true
).trim()
echo "Number of Cypress tests failed: ${CYPRESS_ERROR_CODE}"
if (CYPRESS_ERROR_CODE.toInteger() != 0) {
error("Cypress tests failed: ${CYPRESS_ERROR_CODE}")
}
}
},
sh "docker-compose ${composeBuild} ps"
docker-compose ${composeBuild} ps -q # Only print the IDS
| xargs docker inspect -f '{{ .State.ExitCode }}' # Get all the exit codes
| grep -v '^0' # Invert match anything other than 0
| wc -l # Line count those results
| tr -d ' ' # Remove all the spaces
Parse it to Integer, and process in Jenkins
if (CYPRESS_ERROR_CODE.toInteger() != 0) {
error("Cypress tests failed: ${CYPRESS_ERROR_CODE}")
}
Test run | Time |
---|---|
Consecutive | ~10 mins |
Homebrew parallelisation | ~7 mins |
Jenkins parallelisation | ~4.5 mins |
Docker parallelisation | ~3.5 mins |
or
If you can't spare the resources, your tests will only perform slower
Please enjoy the opening tune for the rest of your day!
@bahmutov
@richyflowers