Testing workflow

Path to file (view file on GitHub)

The original file is located at the following path:

.github / workflows / CI_pytest.yml 

This GitHub Actions workflow runs the automated test suite of uvlhub on every push and pull request that targets main. It starts a MariaDB service container, installs the Python dependencies, installs the rosemary CLI in editable mode and runs pytest over the feature tree.

Table of contents

  1. Workflow name
  2. Triggers
  3. Job
  4. Environment variables
  5. Service container
  6. Steps
    1. 1. Checkout
    2. 2. Set up Python
    3. 3. Install dependencies
    4. 4. Run tests
  7. What actually runs in CI
  8. Selecting a single layer

Workflow name

  • name: Pytest

Do not rename this workflow

The deployment workflow CD_webhook.yml chains off this one through a workflow_run trigger that matches the workflow by its name:

on:
  workflow_run:
    workflows:
      - "Pytest"

If you rename the Pytest workflow, deployments silently stop firing.

Triggers

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

The workflow only runs for main. A push to a feature branch does not run the tests; the tests run when the pull request that targets main is opened or updated, and again when the merge lands on main.

Job

  • pytest: runs on ubuntu-24.04.

The runner image is pinned to an explicit Ubuntu version rather than ubuntu-latest, so a new runner image release cannot change the environment underneath you.

Environment variables

The job declares the database configuration once at job level, and the service container reuses it through ${{ env.* }}:

Variable Value
MARIADB_ROOT_PASSWORD uvlhub_root_password
MARIADB_DATABASE uvlhubdb_test
MARIADB_TEST_DATABASE uvlhubdb_test
MARIADB_USER uvlhub_user
MARIADB_PASSWORD uvlhub_password
MARIADB_HOSTNAME 127.0.0.1
MARIADB_PORT 3306
FLASK_ENV testing

FLASK_ENV: testing makes the application read MARIADB_TEST_DATABASE, so the tests never touch a development database.

Service container

A MariaDB container is started alongside the job:

services:
  mariadb:
    image: mariadb:12.0.2
    env:
      MARIADB_ROOT_PASSWORD: ${{ env.MARIADB_ROOT_PASSWORD }}
      MARIADB_DATABASE: ${{ env.MARIADB_DATABASE }}
      MARIADB_USER: ${{ env.MARIADB_USER }}
      MARIADB_PASSWORD: ${{ env.MARIADB_PASSWORD }}
    ports:
      - 3306:3306
    options: >-
      --health-cmd="mariadb-admin ping -u root -p$MARIADB_ROOT_PASSWORD"
      --health-interval=10s
      --health-timeout=5s
      --health-retries=3

The health check uses mariadb-admin, which is the MariaDB client binary. The job does not start running steps until the health check passes, so the database is ready before the first test connects.

Steps

1. Checkout

- name: Checkout
  uses: actions/checkout@v5

2. Set up Python

- name: Set up Python
  uses: actions/setup-python@v6
  with:
    python-version: '3.13'

Python 3.13 is not optional here. The root pyproject.toml declares requires-python = ">=3.13" and splent_framework requires it.

3. Install dependencies

- name: Install dependencies
  run: |
    python -m pip install --upgrade pip
    pip install -r requirements.txt
    pip install -e ./rosemary
  • requirements.txt pins the application dependencies, including splent_framework, which provides the base classes the features import.
  • pip install -e ./rosemary installs the CLI from rosemary/, which has its own rosemary/pyproject.toml. The path is ./rosemary, not ./.

4. Run tests

- name: Run Tests
  run: pytest app/features/ --ignore-glob='*selenium*'

That is the whole test step. There is no separate environment preparation step: the environment variables are already declared at job level, so pytest inherits them.

You can run exactly the same command locally, from inside web_app_container, where the dependencies are installed and MariaDB is reachable:

docker exec -it web_app_container pytest app/features/ --ignore-glob='*selenium*'

What actually runs in CI

Tests live next to the feature that they exercise, under app/features/<feature>/tests/. Each pytest module declares its layer with a module-level marker, for example:

pytestmark = pytest.mark.integration

The markers themselves are declared in the root pyproject.toml under [tool.pytest.ini_options].

File Marker Runs in CI
test_unit.py unit Yes
test_repository.py repository Yes
test_service.py service Yes
test_integration.py integration Yes
test_selenium.py e2e No, excluded by --ignore-glob='*selenium*'
locustfile.py none (Locust, not pytest) No, not matched by python_files

Browser-driven tests are excluded because they need a running Selenium Grid. Locustfiles are not pytest modules at all: they define Locust HttpUser and TaskSet classes, and python_files = ["test_*.py"] means pytest never collects them, so they carry no marker. The load marker is declared in pyproject.toml but is not applied to anything. Both layers are run on demand instead. See Testing for how to run those layers locally.

Selecting a single layer

Because every pytest module carries a marker, you can narrow a CI failure locally without editing files:

docker exec -it web_app_container pytest app/features/ -m unit
docker exec -it web_app_container pytest app/features/ -m "repository or service"
docker exec -it web_app_container pytest app/features/dataset/tests/test_integration.py