Continuous Integration

Unit testing is one of the most useful practices you can incorporate to improve your code’s quality. Testing is so important that it is best to automate the process so your tests run every time changes are made. This way, if something breaks, you can identify exactly when the code stopped working and get to the bottom of it more easily.

Testing python code

.github/workflows/build-python.yml
name: build python package

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
1    strategy:
      matrix:
2        python-version: ["3.11", "3.12"]

    steps:
      - uses: actions/checkout@v4
      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v3
3        with:
          python-version: ${{ matrix.python-version }}
      - name: Install dependencies
        run: |
          python -m pip install ./python-package[test] --upgrade pip
      - name: Test
        run: |
          python -m pytest
1
Use a matrix strategy to run the job multiple times
2
The build job will run once for Python version 3.11 and once for version 3.12
3
with: allows you to define variables that are used by the action. The Python version from the matrix is passed along to the setup-python action.

Add this workflow and the example code in python-package to a new branch in your repo. Push your branch and open a pull request (PR). The workflow will begin running for your latest commit in the PR branch.

When the workflow run fails

log for “build python package”

Take a look at the workflow status: a green check ✅ means it passed and a red X ❌ means it failed. Let’s investigate the logs to find out why it failed. Click on one of the build jobs and take a look at the pytest step.

log for the failing build job

The output of pytest shows that one of our tests failed. Does it also fail when we run pytest locally?

# first install the local version of the package
pip install -e ./python-package
# then try running pytest
python -m pytest
============================= test session starts ==============================
platform darwin -- Python 3.11.9, pytest-8.3.2, pluggy-1.5.0
rootdir: /Users/sovacoolkl/projects/CCBR/gh-actions-sandbox
plugins: cov-5.0.0, anyio-4.4.0
collected 2 items

python-package/tests/test_main.py .F                                     [100%]

=================================== FAILURES ===================================
___________________________________ test_add ___________________________________

    def test_add():
>       assert add(2, 3) == 5
E       assert -1 == 5
E        +  where -1 = add(2, 3)

python-package/tests/test_main.py:7: AssertionError
=========================== short test summary info ============================
FAILED python-package/tests/test_main.py::test_add - assert -1 == 5
========================= 1 failed, 1 passed in 0.03s ==========================

Oops, we seem to have a bug in our code! Edit python-package/src/mypkg/main.py and fix the bug.

Hint

There’s something wrong with the add() function.

After you fix the bug and save the source file, try running pytest to make sure it’s really fixed this time:

python -m pytest
============================= test session starts ==============================
platform darwin -- Python 3.11.9, pytest-8.3.2, pluggy-1.5.0
rootdir: /Users/sovacoolkl/projects/CCBR/gh-actions-sandbox
plugins: cov-5.0.0, anyio-4.4.0
collected 2 items

python-package/tests/test_main.py ..                                     [100%]

============================== 2 passed in 0.02s ===============================

Great, all the tests passed! Now we can commit and push the fix to your branch. Notice how the workflow re-runs when you push the latest commit. Does the workflow complete successfully this time?

You can setup a branch protection rule1 to require that this workflow passes before Pull Requests can be merged into your default branch. This way, you can guarantee that all contributions pass the unit tests.

More CI workflows

Take a look at these full-featured examples for testing Python and R packages: