2022-05-31 15:36:54 +03:00
# CI/CD Integration
2023-08-07 10:46:29 +03:00
Up until now, we have run our test files locally. Now, we want to integrate them in a CI/CD pipeline
(like [GitHub Actions] or [GitLab CI/CD pipelines]). As Hurl is very fast, we're going to run our tests on each commit
of our project, drastically improving the project quality.
2022-05-31 15:36:54 +03:00
A typical web project pipeline is:
2022-09-28 11:24:24 +03:00
- build the application, run unit tests and static code analysis,
2022-05-31 15:36:54 +03:00
- publish the application image to a Docker registry,
- pull the application image and run integration tests.
2023-08-07 10:46:29 +03:00
In this workflow, we're testing the same image that will be used and deployed in production.
2022-05-31 15:36:54 +03:00
> For the tutorial, we are skipping build and publication phases and
> only run integration tests on a prebuilt Docker image. To check a complete
2023-07-21 14:28:26 +03:00
> project with build, Docker upload/publish and integration tests, go to <https://github.com/jcamiel/hurl-express-tutorial>
2022-05-31 15:36:54 +03:00
2023-08-07 10:46:29 +03:00
In a first step, we're going to write a shell script that will pull our Docker image, launch it and run Hurl tests
against it. Once we have checked that this script runs locally, we'll see how to run it automatically in a CI/CD
pipeline.
## Templating Tests
Before writing our test script, we're going to template our Hurl files so we can run them more easily in various
configuration. One way to do this is to use [variables]. We've already seen variables when [chaining requests],
we're going to see how we can use them to inject data.
In the file `basic.hurl` , we first test the home page:
```hurl
# Checking our home page:
GET http://localhost:3000
# ...
```
We've hardcoded our server's URL but what if we need to run the same test on another URL (against production
URL with HTTPS for example)? We can use a variable like this:
```hurl
# Checking our home page:
GET {{host}}
# ...
```
And run our file with [`--variable`] option:
```shell
$ hurl --variable host=http://localhost:3000 --test basic.hurl
```
This way, our host is not hardcoded any more and we can run our tests in various configurations.
1. Replace `http://localhost:3000` by `{{host}}` in `basic.hurl` , `login.hurl` and `signup.hurl` and test that everything is ok
```shell
$ hurl --variable host=http://localhost:3000 --test *.hurl
Running Hurl tests
[1mintegration/basic.hurl [0m: [1;36mRunning [0m [1/2]
[1mintegration/basic.hurl [0m: [1;32mSuccess [0m (4 request(s) in 18 ms)
[1mintegration/login.hurl [0m: [1;36mRunning [0m [2/2]
[1mintegration/login.hurl [0m: [1;32mSuccess [0m (6 request(s) in 18 ms)
--------------------------------------------------------------------------------
Executed files: 2
Succeeded files: 2 (100.0%)
Failed files: 0 (0.0%)
Duration: 48 ms
```
Now, we're ready to write our integration script.
2022-05-31 15:36:54 +03:00
## Integration Script
2023-07-21 14:28:26 +03:00
1. First, create a directory name `movies-project` , add [`integration/basic.hurl`]
2022-05-31 15:36:54 +03:00
and [`integration/create-quiz.hurl`] from the previous tutorial to the directory.
2023-08-07 10:46:29 +03:00
< pre > < code class = "language-shell" > <!-- no - escape --> $ mkdir movies-project
2023-07-21 14:28:26 +03:00
$ cd movies-project
2022-05-31 15:36:54 +03:00
$ mkdir integration
$ vi integration/basic.hurl
2023-07-21 14:28:26 +03:00
# Import <a href="https://github.com/jcamiel/hurl-express-tutorial/raw/main/integration/basic.hurl">basic.hurl</a> here!
2022-05-31 15:36:54 +03:00
2023-07-21 14:28:26 +03:00
$ vi integration/login.hurl
2022-05-31 15:36:54 +03:00
2023-08-07 10:46:29 +03:00
# Import <a href="https://github.com/jcamiel/hurl-express-tutorial/raw/main/integration/login.hurl">login.hurl</a> here!
$ vi integration/signup.hurl
# Import <a href="https://github.com/jcamiel/hurl-express-tutorial/raw/main/integration/signup.hurl">signup.hurl</a> here!</code></pre>
2022-05-31 15:36:54 +03:00
2022-09-28 11:24:24 +03:00
Next, we are going to write the first version of our integration script that will
2023-08-07 10:46:29 +03:00
just pull the Quiz image and run it. This script will our server URl as argument
2022-05-31 15:36:54 +03:00
2. Create a script named `bin/integration.sh` with the following content:
```bash
2023-08-07 10:46:29 +03:00
#!/bin/sh
2022-05-31 15:36:54 +03:00
set -eu
2023-07-21 14:28:26 +03:00
echo "Starting container"
docker run --name movies --rm --detach --publish 3000:3000 ghcr.io/jcamiel/hurl-express-tutorial:latest
2022-05-31 15:36:54 +03:00
```
3. Make the script executable and run it:
```shell
$ chmod u+x bin/integration.sh
2023-08-07 10:46:29 +03:00
$ bin/integration.sh http://localhost:3000
2023-07-21 14:28:26 +03:00
Starting container
2023-07-05 17:39:57 +03:00
5d311561828d6078e84eb4b8b87dfd5d67bde6d9614ad83860b60cf310438d2a
2022-05-31 15:36:54 +03:00
```
4. Verify that our container is up and running, and stop it.
```shell
$ docker ps
2023-07-21 14:28:26 +03:00
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4002ce42e507 ghcr.io/jcamiel/hurl-express-tutorial:latest "node dist/bin/www.js" 3 seconds ago Up 2 seconds 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp movies
$ docker stop movies
movies
2022-05-31 15:36:54 +03:00
```
2023-08-07 10:46:29 +03:00
Now, we have a basic script that starts our container. Before adding our integration tests, we need to ensure that our
application server is ready: the container has started, but the application server can take a few seconds to be
2022-05-31 15:36:54 +03:00
really ready to accept incoming HTTP requests.
2022-09-28 11:34:00 +03:00
To do so, we can test our health API. With a function `wait_for_url` ,
2022-10-31 13:50:22 +03:00
we use Hurl to check a given URL to return a `200 OK` . We loop on this function
2022-12-19 23:30:08 +03:00
until the check succeeds with [`--retry`] Hurl option. Once the test has succeeded, we stop the container.
2022-05-31 15:36:54 +03:00
5. Modify `bin/integration.sh` to wait for the application to be ready:
```bash
2023-08-07 10:46:29 +03:00
#!/bin/sh
2022-05-31 15:36:54 +03:00
set -eu
wait_for_url () {
2022-12-19 23:30:08 +03:00
echo "Testing $1..."
2023-08-07 10:46:29 +03:00
printf 'GET %s\nHTTP 200' "$1" | hurl --retry "$2" > /dev/null;
2022-12-19 23:30:08 +03:00
return 0
2022-05-31 15:36:54 +03:00
}
2023-07-21 14:28:26 +03:00
echo "Starting container"
docker run --name movies --rm --detach --publish 3000:3000 ghcr.io/jcamiel/hurl-express-tutorial:latest
2022-05-31 15:36:54 +03:00
2023-07-21 14:28:26 +03:00
echo "Waiting server to be ready"
2023-08-07 10:46:29 +03:00
wait_for_url "$1" 60
2022-05-31 15:36:54 +03:00
2023-07-21 14:28:26 +03:00
echo "Stopping container"
docker stop movies
2022-05-31 15:36:54 +03:00
```
2023-08-07 10:46:29 +03:00
We have now the simplest integration test script: it pulls our Docker image, then starts the container and waits for a `200 OK` response.
2022-05-31 15:36:54 +03:00
Next, we're going to add our Hurl tests to the script.
2022-09-25 13:52:24 +03:00
6. Modify `bin/integration.sh` to add integration tests:
2022-05-31 15:36:54 +03:00
```bash
2023-08-07 10:46:29 +03:00
#!/bin/sh
2022-05-31 15:36:54 +03:00
set -eu
# ...
2023-07-21 14:28:26 +03:00
echo "Starting container"
2022-05-31 15:36:54 +03:00
# ...
2023-07-21 14:28:26 +03:00
echo "Waiting server to be ready"
2022-05-31 15:36:54 +03:00
# ...
echo "Running Hurl tests"
2023-08-07 10:46:29 +03:00
hurl --variable host="$1" --test integration/*.hurl
2022-05-31 15:36:54 +03:00
2023-07-21 14:28:26 +03:00
echo "Stopping container"
2022-05-31 15:36:54 +03:00
# ...
```
7. Run [`bin/integration.sh`] to check that our application passes all tests:
```shell
2023-08-07 10:46:29 +03:00
$ bin/integration.sh http://localhost:3000
2023-07-21 14:28:26 +03:00
Starting container
2022-05-31 15:36:54 +03:00
48cf21d193a01651fc42b80648abdb51dc626f31c3f9c8917aea899c68eb4a12
2023-07-21 14:28:26 +03:00
Waiting server to be ready
Testing http://localhost:3000
2022-05-31 15:36:54 +03:00
Running Hurl tests
2022-08-16 16:30:48 +03:00
[1mintegration/basic.hurl [0m: [1;36mRunning [0m [1/2]
2022-08-23 19:36:47 +03:00
[1mintegration/basic.hurl [0m: [1;32mSuccess [0m (4 request(s) in 18 ms)
2023-07-21 14:28:26 +03:00
[1mintegration/login.hurl [0m: [1;36mRunning [0m [2/2]
[1mintegration/login.hurl [0m: [1;32mSuccess [0m (6 request(s) in 18 ms)
2022-05-31 15:36:54 +03:00
--------------------------------------------------------------------------------
2022-08-23 19:36:47 +03:00
Executed files: 2
Succeeded files: 2 (100.0%)
Failed files: 0 (0.0%)
Duration: 48 ms
2023-07-21 14:28:26 +03:00
Stopping container
movies
2022-05-31 15:36:54 +03:00
```
Locally, our test suite is now fully functional. As Hurl is very fast, we can use
it to ensure that new developments don't have regression. Our next step is to run
the integration tests automatically in a CI/CD pipeline. As an example, we're going
2022-12-15 13:06:33 +03:00
to create a [GitHub Action]. You can also see how to integrate your tests in [GitLab CI/CD here].
2022-05-31 15:36:54 +03:00
## Running Tests with GitHub Action
2023-07-21 14:28:26 +03:00
1. Create a new empty repository in GitHub, named `movies-project` :
2022-05-31 15:36:54 +03:00
2023-07-21 14:54:15 +03:00
< div class = "picture" >
2023-08-07 10:46:29 +03:00
< picture >
< source srcset = "/docs/assets/img/github-new-repository-light.avif" type = "image/avif" >
< source srcset = "/docs/assets/img/github-new-repository-light.webp" type = "image/webp" >
< source srcset = "/docs/assets/img/github-new-repository-light.png" type = "image/png" >
< img class = "light-img u-drop-shadow u-border u-max-width-100" src = "/docs/assets/img/github-new-repository-light.png" width = "680" alt = "Create new GitHub repository" / >
< / picture >
< picture >
< source srcset = "/docs/assets/img/github-new-repository-dark.avif" type = "image/avif" >
< source srcset = "/docs/assets/img/github-new-repository-dark.webp" type = "image/webp" >
< source srcset = "/docs/assets/img/github-new-repository-dark.png" type = "image/png" >
< img class = "dark-img u-drop-shadow u-border u-max-width-100" src = "/docs/assets/img/github-new-repository-dark.png" width = "680" alt = "Create new GitHub repository" / >
< / picture >
2023-07-21 14:54:15 +03:00
< / div >
2022-05-31 15:36:54 +03:00
2023-07-21 14:28:26 +03:00
2. On your computer, create a git repo in `movies-project` directory and
2022-05-31 15:36:54 +03:00
commit the projects files:
```shell
$ git init
2023-07-21 14:28:26 +03:00
Initialized empty Git repository in /Users/jc/Documents/Dev/movies-project/.git/
2022-05-31 15:36:54 +03:00
$ git add .
$ git commit -m "Add integration tests."
[master (root-commit) ea3e5cd] Add integration tests.
3 files changed, 146 insertions(+)
create mode 100755 bin/integration.sh
...
2023-07-21 14:28:26 +03:00
$ git remote add origin https://github.com/jcamiel/movies-project.git
2023-08-07 10:46:29 +03:00
$ git push --set-upstream origin main
2022-05-31 15:36:54 +03:00
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
...
2023-07-05 17:39:57 +03:00
```
2022-05-31 15:36:54 +03:00
Next, we are going to add a GitHub Action to our repo. The purpose of this action
will be to launch our integration script on each commit.
2023-08-07 10:46:29 +03:00
3. Create a file in `.github/workflows/ci.yml` :
2022-05-31 15:36:54 +03:00
```yaml
name: CI
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout
2023-07-21 14:28:26 +03:00
uses: actions/checkout@v3
2022-05-31 15:36:54 +03:00
- name: Build
run: echo "Building app..."
- name: Integration test
run: |
2023-07-21 14:28:26 +03:00
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.0.0/hurl_4.0.0_amd64.deb
sudo dpkg -i hurl_4.0.0_amd64.deb
2023-08-07 10:46:29 +03:00
bin/integration.sh http://localhost:3000
2022-05-31 15:36:54 +03:00
```
4. Commit and push the new action:
```shell
$ git add .github/workflows/ci.yml
$ git commit -m "Add GitHub action."
[main 077d754] Add GitHub action.
1 file changed, 19 insertions(+)
...
$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
...
```
Finally, you can check on GitHub that our action is running:
2023-07-21 14:54:15 +03:00
< div class = "picture" >
2023-08-07 10:46:29 +03:00
< picture >
< source srcset = "/docs/assets/img/github-action-light.avif" type = "image/avif" >
< source srcset = "/docs/assets/img/github-action-light.webp" type = "image/webp" >
< source srcset = "/docs/assets/img/github-action-light.png" type = "image/png" >
< img class = "light-img u-drop-shadow u-border u-max-width-100" src = "/docs/assets/img/github-action-light.png" width = "680" alt = "GitHub Action" / >
< / picture >
< picture >
< source srcset = "/docs/assets/img/github-action-dark.avif" type = "image/avif" >
< source srcset = "/docs/assets/img/github-action-dark.webp" type = "image/webp" >
< source srcset = "/docs/assets/img/github-action-dark.png" type = "image/png" >
< img class = "dark-img u-drop-shadow u-border u-max-width-100" src = "/docs/assets/img/github-action-dark.png" width = "680" alt = "GitHub Action" / >
< / picture >
2023-07-21 14:54:15 +03:00
< / div >
2022-05-31 15:36:54 +03:00
2022-12-15 13:06:33 +03:00
## Running Tests with GitLab CI/CD
2023-08-07 10:46:29 +03:00
1. Create a new empty repository in GitLab, named `movies-project` :
< div class = "picture" >
< picture >
< source srcset = "/docs/assets/img/gitlab-new-repository-light.avif" type = "image/avif" >
< source srcset = "/docs/assets/img/gitlab-new-repository-light.webp" type = "image/webp" >
< source srcset = "/docs/assets/img/gitlab-new-repository-light.png" type = "image/png" >
< img class = "light-img u-drop-shadow u-border u-max-width-100" src = "/docs/assets/img/gitlab-new-repository-light.png" width = "680" alt = "Create new GitLab repository" / >
< / picture >
< picture >
< source srcset = "/docs/assets/img/gitlab-new-repository-dark.avif" type = "image/avif" >
< source srcset = "/docs/assets/img/gitlab-new-repository-dark.webp" type = "image/webp" >
< source srcset = "/docs/assets/img/gitlab-new-repository-dark.png" type = "image/png" >
< img class = "dark-img u-drop-shadow u-border u-max-width-100" src = "/docs/assets/img/gitlab-new-repository-dark.png" width = "680" alt = "Create new GitLab repository" / >
< / picture >
< / div >
2. On your computer, create a git repo in `movies-project` directory and
commit the projects files:
```shell
$ git init
Initialized empty Git repository in /Users/jc/Documents/Dev/movies-project/.git/
$ git add .
$ git commit -m "Add integration tests."
[master (root-commit) ea3e5cd] Add integration tests.
3 files changed, 146 insertions(+)
create mode 100755 bin/integration.sh
...
$ git remote add origin git@gitlab.com:jcamiel/movies-project.git
$ git push --set-upstream origin main
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
...
```
Next, we are going to add a GitLab CI/CD pipeline. The purpose of this pipeline will be to launch our integration
script on each commit. We'll base our image on a Docker based image, with a [Docker-In-Docker service].
3. Create a file `.gitlab-ci.yml` :
```yaml
image: docker:24
build:
stage: build
services:
- docker:24-dind
before_script:
# Add Hurl on Alpine (testing channel)
- apk add --no-cache -X http://dl-cdn.alpinelinux.org/alpine/edge/testing hurl
script:
- bin/integration.sh http://docker:3000
```
> Because of Docker-In-Docker, our server is accessible with the `docker` hostname (and not `localhost`). As we have
> made our script configurable, we can just pass the hostname and don't modify our integration script
4. Commit and push the new action:
```shell
$ git add .gitlab-ci.yml
$ git commit -m "Add GitLab CI/CD pipeline."
[main 11c4e7e] Add GitLab CI/CD pipeline.
1 file changed, 13 insertions(+)
create mode 100644 .gitlab-ci.yml
$ git push
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
...
```
Finally, you can check on GitLab that our pipeline is running:
< div class = "picture" >
< picture >
< source srcset = "/docs/assets/img/gitlab-pipeline-light.avif" type = "image/avif" >
< source srcset = "/docs/assets/img/gitlab-pipeline-light.webp" type = "image/webp" >
< source srcset = "/docs/assets/img/gitlab-pipeline-light.png" type = "image/png" >
< img class = "light-img u-drop-shadow u-border u-max-width-100" src = "/docs/assets/img/gitlab-pipeline-light.png" width = "680" alt = "GitHub Action" / >
< / picture >
< picture >
< source srcset = "/docs/assets/img/gitlab-pipeline-dark.avif" type = "image/avif" >
< source srcset = "/docs/assets/img/gitlab-pipeline-dark.webp" type = "image/webp" >
< source srcset = "/docs/assets/img/gitlab-pipeline-dark.png" type = "image/png" >
< img class = "dark-img u-drop-shadow u-border u-max-width-100" src = "/docs/assets/img/gitlab-pipeline-dark.png" width = "680" alt = "GitHub Action" / >
< / picture >
< / div >
For a more complete [GitLab CI/CD] example, you can check [this detailed tutorial] on how to continuously run your Hurl test suite.
2022-12-15 13:06:33 +03:00
2022-05-31 15:36:54 +03:00
## Tests Report
TBD
## Recap
2022-09-28 11:24:24 +03:00
In less than half an hour, we have added a full CI/CD pipeline to our project.
2022-05-31 15:36:54 +03:00
Now, we can add more Hurl tests and start developing new features with confidence!
[`integration/basic.hurl`]: https://raw.githubusercontent.com/jcamiel/quiz/master/integration/basic.hurl
[`integration/create-quiz.hurl`]: https://raw.githubusercontent.com/jcamiel/quiz/master/integration/create-quiz.hurl
[GitHub Actions]: https://github.com/features/actions
[GitLab CI/CD pipelines]: https://docs.gitlab.com/ee/ci/pipelines/
[`bin/integration.sh`]: https://github.com/jcamiel/quiz/blob/master/bin/integration.sh
2022-12-15 13:06:33 +03:00
[GitLab CI/CD here]: https://about.gitlab.com/blog/2022/12/14/how-to-continously-test-web-apps-apis-with-hurl-and-gitlab-ci-cd/
[GitLab CI/CD]: https://about.gitlab.com/why-gitlab/
[this detailed tutorial]: https://about.gitlab.com/blog/2022/12/14/how-to-continously-test-web-apps-apis-with-hurl-and-gitlab-ci-cd/
2023-02-06 16:20:01 +03:00
[`--retry`]: /docs/manual.md#retry
2023-08-07 10:46:29 +03:00
[variables]: /docs/templates.md#variables
[chaining requests]: /docs/tutorial/chaining-requests.md
[Docker-In-Docker service]: https://docs.gitlab.com/ee/ci/docker/
[`--variable`]: /docs/manual.md#variable