unittest directory structure – cannot import src code

Question:

I have the following folder structure in my project:

my-project
   src/
       __init__.py
       script.py
   test/
       __init__.py
       test_script.py

Ideally I want to have a separate folder where all the unit tests go. My test_script.py looks something like this:

from src.script import my_object

class TestClass(unittest.TestCase):
    
    def test_script_object(self):
        # unit test here
        pass

if __name__ == 'main':
    unittest.main()

When I try to run the script (using python test_script.py) I get the following error:

Traceback (most recent call last):
  File "test_script.py", line 4, in <module>
    from src.script import my_object
ModuleNotFoundError: No module named 'src'

I was following the instructions from this other thread, and I even tried appending src to the sys path (which forces me to change how I do imports in the rest of the project). When I’m not trying to append to the sys path, both of my __init__.py files are empty.

I am using python 3.8.

Does anyone have any suggestions? I’m new to unit testing in python, so maybe there is a better structure or other conventions I’m not aware of. Thanks in advance for your help!

Asked By: ellen

||

Answers:

Generally, any instructions that have you modifying sys.path in order to run your tests are sending you in the wrong direction. Your testing tool should be able to discover both your tests and your application code without requiring that sort of hackery.

I generally use pytest for running my tests. I would structure your example like this:

my-project/
├── src
│   ├── conftest.py
│   └── myproject
│       ├── __init__.py
│       └── script.py
└── tests
    ├── __init__.py
    └── test_script.py

Assuming that src/myproject/script.py looks like this:

def double(x: int):
    return x*2

And tests/test_script.py look like this:

import myproject.script

def test_double():
    res = myproject.script.double(2)
    assert res == 4

I can run tests from the my-project directory by simply running pytest:

$ pytest
========================================== test session starts ==========================================
platform linux -- Python 3.11.1, pytest-7.2.0, pluggy-1.0.0
rootdir: /home/lars/tmp/python/my-project
collected 1 item

tests/test_script.py .                                                                            [100%]

=========================================== 1 passed in 0.00s ===========================================

In this setup, the file src/conftest.py is what allows pytest to automatically discover the location of your application code. Alternatively, you could instead specify that in your pyproject.toml file like this:

[tool.pytest.ini_options]
pythonpath = [
  "src"
]

pytest also works if you write unittest-style tests as you show in your question; the above behavior would be identical if tests/test_script.py looked like:

import unittest
import myproject.script

class TestScript(unittest.TestCase):
    def test_double(self):
        res = myproject.script.double(2)
        self.assertEqual(res, 4)

if __name__ == '__main__':
    unittest.main()

(But I prefer pytest’s simpler syntax.)

Possibly useful reading:


If you really want to use unittest you can use the same directory layout, but you will need to update sys.path for it to run correctly. The easiest way to do that is:

PYTHONPATH=$PWD/src python -m unittest

This will automatically discover all your tests and run them. You can run a single test like this:

PYTHONPATH=$PWD/src python -m tests.test_script

You can avoid needing to modify sys.path if you use a simpler directory layout:

my-project/
├── myproject
│   ├── __init__.py
│   └── script.py
└── tests
    ├── __init__.py
    └── test_script.py

Both pytest and python -m unittest, when run from the my-project directory, will correctly discover and run your tests without requiring any path modifications.

Answered By: larsks