avatarLynn G. Kwong

Summary

The provided content outlines how to automate Python code testing and enforcing coding standards using GitLab CI pipelines through a practical FastAPI project example.

Abstract

The web content discusses the utilization of GitLab CI pipelines for automated code checking and testing in Python projects. It introduces a simple FastAPI application with a single endpoint and a unit test file to demonstrate the process. The article explains the setup of a GitLab CI pipeline with two stages: static code checking using the black formatter and unit testing with pytest. The pipeline ensures that code adheres to a common standard and passes tests before merging, which cannot be bypassed like client-side checks. The author emphasizes the importance of server-side code checking for maintaining code quality within a group, noting that while GitLab CI/CD pipelines offer extensive capabilities, the example provided focuses on the CI aspects, leaving the CD part to cloud-specific tools such as GCP Cloud Build.

Opinions

  • The author believes that server-side code checking is crucial for enforcing a common code standard within a group, as it cannot be skipped like client-side checks.
  • GitLab CI/CD pipelines are considered powerful and versatile tools, but the author's group primarily uses them for CI (Continuous Integration) to check code format and run unit tests.
  • The article suggests that the GitLab CI pipeline helps maintain code quality by running automated checks and tests, ensuring that only verified code is merged.
  • The author implies that while GitLab's CI/CD features are comprehensive, specific use cases like CD (Continuous Deployment) may be better handled by specialized cloud tools.
  • There is an implied preference for using black for code formatting and pytest for unit testing in Python projects, indicating their effectiveness in the CI process.

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

Image by Mohamed_hassan on Pixabay

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:

  • --gitignore means to filter results by using the .gitignore file. With this option, we can have a clean file structure as shown above.
  • -I .git means to not show the .git folder, which is not covered by .gitignore.
  • -a means 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.py

In 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 the image and before_script keywords for each subsequent job.
  • image —Specify a Docker image that the job runs in. Since the image is specified under the default key, 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, namely static code checking and unit tests.
  • static-code-checking and unit-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.

Related posts:

Python
Gitlab
Fastapi
Cicd
Unittest
Recommended from ReadMedium