Day 1: Test and CI for MoreBeautifulPython
Tags: testing, python, opensource, programming
Published: 2022-12-01 12:39:00

---
title: Day 1: Test and CI for MoreBeautifulPython
published: true
description: 
tags: testing, python, opensource, programming
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
published_at: 2022-12-01 12:39 +0000
canonical_url: https://code-maven.com/test-and-ci-for-more-beautiful-python
series: ci-2022-12
---

For the first day of the [2022 December CI Challenge](/szabgab/the-2022-december-ci-challenge-5dof) I was looking for something relatively easy.

This was partially successful though I ended up with a project that did not have tests yet, so I also had to write one.


## Finding the project

In order to find a package that does not have CI I went over the first page of [Has GitHub but no CI](https://pydigger.com/search/has-github-no-ci) on the [PyDigger](https://pydigger.com/).

I saw a few projects with Machine Learning, I skipped over them as those probably include a lot more CPU power and installation than other projects.

I saw some CLI tools. Those are much more light-weight, but testing CLI in a headless environment might be tricky. So I skipped these too for now.

Web scraping depends on some web site that might change and if it is in Japanese that would be extra difficult for me who does not speak Japanese.

API clients will probably need some API secret.

Finally I found a project that seemed reasonably simple to work on. It is called [MoreBeautifulPython](https://github.com/sudongqi/MoreBeautifulPython).


## How to run the tests?

One thing I noticed with Python projects is that I rarely see instructions in the README or anywhere else on how to
run the tests of the project or how to release it to pypi. That despite the fact that there are **more than one ways to do it in Python**.  Even more than there are in Perl, where this aspect is quite standardized.

In this project I could not find any file with a name starting with **test_** which is the standard naming for test-files in Python.
Luckily the README pointed to an example in the repository called **examples.py**.

I could run it **python examples.py**. It worked and I saw some output. I looked into the file where I saw two functions with a name starting with **test_**. Good, so there might be some test. However the content of those was not very convincing.

I tried `pytest examples.py` but it was broken. I don't know if I ran the test incorrectly or if they are some experiments,
so I [reported the problem](https://github.com/sudongqi/MoreBeautifulPython/issues/1).

So I guess there are no tests.

## Do not import *

Looking at that code, I find the importing everything using `*` disturbing. This is a bad practice as a change in the module being imported (e.g. a new version) could break our code. I know that in this case the author of the imported module is using it, so the chances for the breakage are small, but it is still a dangerous practice. By using this others will also be encouraged to use the **import everything blindly** mode instead of the **explicit import** which is safer and which is much more aligned with the philosophy of Python of **explicit is better than implicit**.

## Adding CI to run the examples

As there are no working test first I decided to add a CI that would only run the `examples.py`. Without even checking its output. This is far from perfect, but at least we'll have a CI in place and that at least it will verify that the example can run on multiple versions of Python and various platforms.

My first attempt failed, because I left in some code in the GitHub Actions configuration file that I copied from the
[GitHub Action for Python skeleton](https://code-maven.com/github-actions) I used.

## Adding a test

However, while GitHub was trying to do its job I started to work on a test.

I had to fiddle quite some time till I figured out how to test it. I think the fact that the code uses multiprocessing
is what made my first attempts to fail, but I have not researched that.

Eventually I create a very small example script I called:

```python
from mbp import log

log('this is from the global logger', end='\n')
```

It does not do much, but at least I import the log function explicitly.

I also added a test file called

```python
import os
import sys
import subprocess
from src.mbp import log

os.environ['PYTHONPATH'] = 'src'

def run_process(command):
    proc = subprocess.Popen(command,
        stdout = subprocess.PIPE,
        stderr = subprocess.PIPE,
    )
    out, err = proc.communicate()
    exit_code = proc.returncode
    return exit_code, out, err


def test_log():
    exit_code, out, err = run_process([sys.executable, 'example_log.py'])
    assert exit_code == 0
    NL ='\r\n' if sys.platform == 'win32' else '\n'
    assert out.decode() == f'this is from the global logger{NL}'
    assert err.decode() == ''
```

In it I added a function called `run_process` that can run an external program and capture its output. I tried it with `capsys` that comes with `pytest` but it did not work. I have not invested too much time into trying to understand why.

After pushing this out to GitHub and letting GitHub Actions do its thing, it still failed on Windows.
I had to deal with the differences in newlines on Windows vs Mac OSX and Linux.
This surprised me a bit, but I did not want to argue with the system so I added some code to expect the appropriate line-ending.

## Installing the requirements

I am used to having `requirements.txt` in a python project and running `pip install -r requirements.txt` to install the dependencies, but this package had its dependencies listed in the `setup.cfg` file. I could not find the way to install the dependencies listed there so I ended up running `pip install .` on the CI server and installing the whole package. Not the deal solution, but it will work for now till someone sends a better solution.


## The CI configuration file

Finally I managed to put together a configuration file as well that I saved here:

```yaml
name: CI

on:
  push:
  pull_request:
  workflow_dispatch:
  schedule:
    - cron: '42 5 * * *'

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        runner: [ubuntu-latest, macos-latest, windows-latest]
        python-version: ["3.9", "3.10", "3.11"]

    runs-on: ${{matrix.runner}}
    name: OS ${{matrix.runner}} Python ${{matrix.python-version}}

    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v4
      with:
        python-version: ${{ matrix.python-version }}

    - name: Install dependencies
      run: pip install .

    - name: Install pytest
      run: pip install pytest

    - name: Check Python version
      run: python -V

    - name: Test with pytest
      run: pytest -vs

    - name: Run examples to see if they work
      run: python examples.py
```


## Pull-Request

I also sent a [pull-request](https://github.com/sudongqi/MoreBeautifulPython/pull/2) where you can also see my individual commits as I tried the whole thing to work together.