Search code examples
pythongithub-actionspython-poetry

Running Python Poetry unit test in Github actions


I have my unittests in a top level tests/ folder for my Python project that uses poetry (link to code). When I run my tests locally, I simply run:

poetry run python -m unittest discover -s tests/

Now, I want to run this as CI in Github Actions. I added the following workflow file:

name: Tests

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

permissions:
  contents: read

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3
    
    - uses: actions/setup-python@v3
      with:
        python-version: "3.9"
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install pylint poetry

    - name: Run tests
      run: poetry run python -m unittest discover -s tests/ -p '*.py'

    - name: Lint
      run: pylint $(git ls-files '*.py')

But, this fails (link to logs):

Run poetry run python -m unittest discover -s tests/ -p '*.py'
Creating virtualenv fixed-income-annuity-WUaNY9r8-py3.9 in /home/runner/.cache/pypoetry/virtualenvs
E
======================================================================
ERROR: tests (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests
Traceback (most recent call last):
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/unittest/loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)
  File "/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/unittest/loader.py", line 377, in _get_module_from_name
    __import__(name)
  File "/home/runner/work/fixed_income_annuity/fixed_income_annuity/tests/tests.py", line 3, in <module>
    from main import Calculator
  File "/home/runner/work/fixed_income_annuity/fixed_income_annuity/main.py", line 6, in <module>
    from dateutil.relativedelta import relativedelta
ModuleNotFoundError: No module named 'dateutil'


----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (errors=1)
Error: Process completed with exit code 1.

Why does this fail in Github Actions but works fine locally? I understand I can go down the venv route to make it work but I prefer poetry run since its a simple 1 liner.


Solution

  • You need to install the dependencies in your action in order for it to work. Adding poetry install after your pip statement is an immediate fix, but there are some further tweaks you should make.

    Your project needs to be tweaked for pytest to pick up your tests. pytest requires that your files be prefixed with test_, and all test classes should start with Test.

    You should have poetry manage pytest and pylint as dev dependencies so that they are installed within the venv only when you include them in your github actions (and locally) by running poetry install --with dev:

    # in pyproject.toml
    [tool.poetry.group.dev.dependencies]
    pytest = "^7.4.3"
    pylint = "^3.0.2"
    

    You'll also want to include the current directory in your pythonpath for pytest:

    # in pyproject.toml
    [tool.pytest.ini_options]
    pythonpath = [
      "."
    ]
    

    You'll also want to add an init-hook for pylint to handle imports correctly while in the venv:

    [MASTER]
    init-hook='import sys; sys.path.append(".")'
    # ...
    

    From there, you can just use poetry to manage both pytest and pylint.

    # snippet of test.yml
    # ...
        - name: Install dependencies
          run: |
            python -m pip install --upgrade pip
            pip install poetry
            poetry install --with dev
    
        - name: Run tests
          run: poetry run pytest
    
        - name: Lint
          run: poetry run pylint $(git ls-files '*.py')