Build a GitLab CI pipeline to Enforce Testing Your Python Code Automatically
Learn the basics of GitLab CI pipelines through a simple but practical example

GitLab is a web-based Git repository that provides free open and private repositories. It is an integrated DevOps platform that offers version control, CI/CD, issue tracking, etc.
In this post, we will introduce how to use the CI pipeline of GitLab to check and test our Python code automatically. Client-side code checking can be skipped with the --no-verify option of Git, which is uncontrollable and makes it difficult to enforce a common standard for code checking in a group. On the other hand, server-side code checking cannot be skipped and thus it’s easier to enforce a common standard.
Build a simple project
We will build a simple FastAPI project that includes a single endpoint and a unit test file:
tree --gitignore -I .git -a . ├── app │ ├── __init__.py │ ├── main.py │ └── test.py ├── .gitignore ├── .gitlab-ci.yml ├── README.md └── requirements.txt
The options of the tree command can be interesting:
--gitignoremeans to filter results by using the.gitignorefile. With this option, we can have a clean file structure as shown above.-I .gitmeans to not show the.gitfolder, which is not covered by.gitignore.-ameans to show hidden files as well.
requirements.txt include the Python libraries that are needed for this project.
black>=24.2.0,<24.3.0
fastapi>=0.110.0,<0.111.0
httpx>=0.27.0,<0.28.0
pytest>=8.1.1,<8.2.0
uvicorn>=0.28.0,<0.29.0- black —A popular Python code formatter.
- fastapi — A modern API framework.
- httpx — A library used to make sync and async HTTP requests. It’s used together with the pytest library to make unit tests for FastAPI applications.
- pytest — A framework to write scalable unit tests.
- uvicorn — An ASGI web server implementation used to spin up FastAPI applications.
Our application file main.py contains a simple API endpoint written with FastAPI:
from fastapi import FastAPI
app = FastAPI()
@app.get("/hello-world")
async def hello_word():
return {"message": "Hello, world!"}To test the application locally, you can create the above files or clone this repo and run the following commands:
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
uvicorn main:app --reload
curl http://127.0.0.1:8000/hello-world
{"message":"Hello, world!"}And finally, we have a test file (test.py) that includes a simple test for our endpoint:
from fastapi.testclient import TestClient
from .main import app
client = TestClient(app)
def test_hello_world():
response = client.get("/hello-world")
assert response.status_code == 200
assert response.json() == {"message": "Hello, world!"}You can run the tests locally with pytest:
pytest app/test.py
Setup GitLab CI pipeline
Now that our demo project is created, we can set up our GitLab CI pipeline, which simply means writing the .gitlab-ci.yml file and putting it in the root folder. Note that the extension is .yml, not .yaml, otherwise, it won’t work.
The .gitlab-ci.yml file has the following content:
default:
image: "python:3.12"
before_script:
- python -m venv venv
- source venv/bin/activate
- pip install -r requirements.txt
stages:
- static code checking
- unit tests
static-code-checking:
stage: static code checking
script:
- black --check app/
unit-tests:
stage: unit tests
script:
- pytest app/test.pyIn this example, we use the basic pipeline which means to run stages sequentially and run jobs in the same stage concurrently. We will explain the keywords from top to bottom:
default— Set global defaults for some keywords. Each default keyword is copied to every job that doesn’t already have it defined. If the job already has a keyword defined, that default is not used. In this example, we set the default values for theimageandbefore_scriptkeywords for each subsequent job.image—Specify a Docker image that the job runs in. Since the image is specified under thedefaultkey, it means this Docker image will be used for all jobs by default.before_script—Define an array of commands that should run before each job’s script commands. In this example, we create a virtual environment and install the libraries in it.stages— Specify the names and order (from top to bottom) of the pipeline stages. In this example, two stages are defined, namelystatic code checkingandunit tests.static-code-checkingandunit-tests— Custom names for the jobs. The names and orders are not important as the order is determined by the stage.stage—Defines which stage a job runs in.script— Specify commands for the runner to execute.
The details for these keywords can be found in this official reference.
Once everything is ready, we can add everything to the repo and push it to GitLab. If everything is set up correctly, the pipeline will be run whenever some code is pushed to GitLab.
Check the pipelines in GitLab
On the page of your GitLab repository, you can open the pipelines page by clicking “Build” => “Pipelines”:

You can see all the historical runs of the pipeline. If a stage fails, the subsequent one cannot be run. For example, if the static code checking stage fails, the unit tests stage will not be run:

We can open a failed job and check the log there:

We can then fix our code by the log. If everything is working properly, our pipeline will pass:

Having server-side code checking is necessary because unlike client-side one it cannot be skipped and thus it’s easier to enforce a common standard in a group. In this post, we scratched the surface of GitLab CI/CD pipelines for server-side code checking with a simple yet practical example. GitLab CI/CD pipelines are very powerful and versatile. However, in my group, we have only used it to do the CI part, namely only to check code format and perform unit tests as we are doing in this example. The more advanced CD part would be handled by the corresponding Cloud CD tools like GCP Cloud Build.





