Testing on software forges

Questions

  • How can we implement automatic testing each time we push changes to the repository?

  • Why is it good to autoclose issues with commit messages?

“Continuous integration”

We will now learn to set up automatic tests using either GitHub Actions or GitLab CI/CD - you can choose which one to use and instructions are provided for both.

This exercise can be run in “collaborative mode” by following instead the instructions in Full collaborative workflow. In the collaborative version steps C-D below are performed by a collaborator.

Exercise CI-1: Create and use a continuous integration workflow on GitHub or GitLab

In this exercise, we will:

  • A. Create and add code to a repository on GitHub/GitLab (or, alternatively, fork and clone an existing example repository)

  • B. Set up tests with GitHub Actions/ GitLab CI/CD

  • C. Find a bug in our repository and open an issue to report it

  • D. Fix the bug on a bugfix branch and open a pull request (GitHub)/ merge request (GitLab)

  • E. Merge the pull/merge request and see how the issue is automatically closed.

  • F. Create a test to increase the code coverage of our tests.

Prerequisites

If you are new to Git, you can find a step-by-step guide to setting up repositories and making commits in this git-refresher material. If you are new to pull requests / merge requests, you can learn all about them in the Collaborative Git lesson.

Step 1: Create a new repository on GitHub/GitLab OR fork from the example repo

Create a new repository

  • Begin by creating a repository called (for example) example-ci.

  • Before you create the repository, select “Initialize this repository with a README” (otherwise you try to clone an empty repo).

  • Clone the repository (git clone git@github.com:<yourGitID>/example-ci.git).

  • Add the following files and code

Add a file functions.py containing:

def add(a, b):
    return a + b

def subtract(a, b):
    return a + b  # <--- fix this in step 7

def multiply(a, b):
    return a * b

def convert_fahrenheit_to_celsius(fahrenheit):
    return multiply(subtract(fahrenheit, 32), 9 / 5) # <-- Fix this in step 7

and a file test_functions.py containing:

from functions import add, subtract, multiply
from functions import convert_fahrenheit_to_celsius as f2c
import pytest

def test_add():
    assert add(2, 3) == 5
    assert add('space', 'ship') == 'spaceship'

# uncomment the following test in step 5
#def test_subtract():
#    assert subtract(2, 3) == -1

# uncomment the following test in step 11
# def test_convert_fahrenheit_to_celsius():
#    assert f2c(32) == 0
#    assert f2c(122) == pytest.approx(50)
#    with pytest.raises(AssertionError):
#        f2c(-600)

Finally, stage the files (git add <filename>), commit (git commit -m "some commit message"), and push the changes (git push origin main).

Fork and clone an existing example repository

  • Fork the example repo. There are two options one for Python and one for R.

  • Clone your fork (git clone git@github.com:<yourGitID>/<Py/R>TestingExample.git).

Step 2: Run tests locally

You can now run your tests locally with

pytest

Step 3: Enable automated testing

In this step we will enable GitHub Actions. Select “Actions” from your GitHub repository page. You get to a page “Get started with GitHub Actions”. Select the button for “Configure” under Python Application:

Selecting a Python workflow

Select “Python application” as the starter workflow.

GitHub creates the following file for you in the subfolder .github/workflows. Modify the highlighted lines according to the action below. This will add a code coverage report to new pull requests. The if clause restricts this to pull requests, as otherwise this action would not have a target to write the reports to. On pushes only the unittesting is run.

# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python

name: Test

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

permissions:
  contents: read
  pull-requests: write

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v4
    - name: Set up Python 3.10
      uses: actions/setup-python@v3
      with:
        python-version: "3.10"
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install flake8 pytest pytest-cov
        if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
    - name: Lint with flake8
      run: |
        # stop the build if there are Python syntax errors or undefined names
        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
        # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
        flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
    - name: Test with pytest
      run: |
        pytest --cov-report "xml:coverage.xml" --cov=.
    - name: Create Coverage
      if: ${{ github.event_name == 'pull_request' }}
      uses: orgoro/coverage@v3
      with:
          coverageFile: coverage.xml
          token: ${{ secrets.GITHUB_TOKEN }}

Commit the change by pressing the “Start Commit” button:

Committing the change

Committing the file via the GitHub web interface: follow the flow, give it some commit name. You can commit directly to master.

Step 4: Verify that tests have been automatically run

Observe in the repository how the test succeeds. While the test is executing, the repository has a yellow marker. This is replaced with a green check mark, once the test succeeds:

Verify that the test passed

Green check means passed.

Also browse the “Actions” tab and look at the steps there and their output.

Step 5: Add a test which reveals a problem

After you committed the workflow file, your GitHub/GitLab repository will be ahead of your local cloned repository. Update your local cloned repository:

$ git pull origin main

Hint: if the above command fails, check whether the branch name on the GitHub/GitLab repository is called main and not perhaps master.

Next uncomment the code in test_functions.py under “step 5”, commit, and push. Verify that the test suite now fails on the “Actions” tab (GitHub) or the “CI/CD->Pipelines” tab (GitLab).

Step 6: Open an issue on GitHub/GitLab

Open a new issue in your repository about the broken test (click the “Issues” button on GitHub or GitLab and write a title for the issue). The plan is that we will fix the issue through a pull/merge request.

Step 7: Fix the broken test

Now fix the code on a new branch, you can call it yourname/bugfix. After you have fixed the code on the new branch, commit the following commit message "restore function subtract; fixes #1" (assuming that you try to fix issue number 1).

Shortcut

Here it’s perfectly possible to take a shortcut and commit and push directly to the main branch. If you do this, steps 8-9 below are skipped.

  • When would you push directly to the main branch, and when would you send a pull/merge request?

Then push to your repository.

Step 8: Open a pull request (GitHub)/ merge request (GitLab)

Go back to the repository on GitHub or GitLab and open a pull/merge request. In a collaborative setting, you could request a code review from collaborators at this stage. Before accepting the pull/merge request, observe how GitHub Actions/ Gitlab CI automatically tested the code.

If you forgot to reference the issue number in the commit message, you can still add it to the pull/merge request: my pull/merge request title, closes #1.

Step 9: Accept the pull/merge request

Observe how accepting the pull/merge request automatically closes the issue (provided the commit message or the pull/merge request contained the correct issue number).

See also:

Discuss whether this is a useful feature. And if it is, why do you think is it useful?

Step 10: Increase your code coverage

We are currently missing several functions in our tests. Write a test for the multiply function in a new branch and create a pull request. On Python you can directly observe the increase in code coverage. On R you can have a look at the action (Actions -> last run of your action -> Select a job -> Test coverage). If you compare this with the previous run, you should see an increase once the update is in.

Step 11 (optional): Repeat steps 5-9 for the convert_fahrenheit_to_celsius function:

Repetition helps learning, so let’s do the testing again for our convert_fahrenheit_to_celsius function. Uncomment the test for the convert_fahrenheit_to_celsius function and repeat steps 5 to 9 fixing the bug this test exposes.

Discussion

Finally, we discuss together about our experiences with this exercise.


Where to go from here

  • This example was using Python but you can achieve the same automation for R or Fortran or C/C++ or other languages

  • This workflow is very useful for collaborators who work on the same code and it works both for centralized and forking workflows - have a look at this alternative exercise to see how that works.

  • GitHub Actions has a Marketplace which offer wide range of automatic workflows

  • On GitLab use GitLab CI

  • For Windows builds you can also use Appveyor

Keypoints

  • When fixing bugs or other problems reported in issues, use the issue autoclosing mechanism when you send the pull/merge request.