Search code examples
pythonpython-importpython-unittest

TextTestRunner doesn't recognize modules when executing tests in a different project


i am currently working on a project, where i need to run tests inside a different file structure like this:

/my_project
├── __init__.py
├── ...my python code
/given_proj
├── __init__.py
├── /package
│ ├── __init__.py
│ └── main.py
└── /tests
  └── test_main.py

Current approach

From inside my project i want to execute the tests within the given project. My current approach is using unittest.TextTestRunner like this: unittest.TextTestRunner().run(unittest.defaultTestLoader.discover('../given_proj/tests')).

Problem with the current approach

Of course the test file wants to import from main.py like this from package.main import my_function. However when i run my code, the tests fail to run because the "package" module cannot be found:

...\given_proj\tests\test_main.py", line 2, in <module>
    from package.main import my_function
ModuleNotFoundError: No module named 'package'

When i run the tests with python -m unittest discover -s tests from the command line in the directory of the given_proj they run fine.

What i tried

I tried changing the working directory to given_proj with os.chdir('../given_proj') however it produces the same result.

What i kinda tried, was importing the module manually with importlib.import_module(). There i am not sure if i did it wrong or it doesnt work either.

My question

How do i make it, that the tests get run, as if i would run it from the actual directory they are supposed to run? Ideally i wouln't need to change the "given_project" at all, because i want to do this with multiple projects.

Reproducing it

I reduced it to a very minimal project, if anybody wants to try and reproduce it. The file-structure is the one at the top of the post.

All __init__.py files are empty.

/my_project/main.py:

import os
import unittest


import os
import unittest


if __name__ == "__main__":
    dirname = "../given_proj/tests" #either "./" or "../" depending of where you run the python file from
    unittest.TextTestRunner().run(unittest.defaultTestLoader.discover(dirname))
                                   

/given_proj/package/main.py:

def my_function(num):
    return num*2

/given_proj/tests/test_main.py:

import unittest
from package.main import my_function

class TestMain(unittest.TestCase):
    def test_my_function(self):
        result = my_function(5)
        self.assertEqual(result, 10)

        result = my_function(10)
        self.assertEqual(result, 20)
        
        result = my_function(0)
        self.assertEqual(result, 0)

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

Solution

  • A possible solution is to add the following instructions in your file test_main.py:

    import unittest
    
    import sys                  # <-- add this import
    sys.path.insert(1, '..')    # <-- add this instruction 
    
    print(sys.path)             # <--- TO CHECK THE CONTENT OF sys.path
    
    #from package.main import my_function       # <--- comment your import
    from given_proj.package.main import my_function    # <--- add this import 
    
    
    class TestMain(unittest.TestCase):
        def test_my_function(self):
            result = my_function(5)
            self.assertEqual(result, 10)
    
            result = my_function(10)
            self.assertEqual(result, 20)
    
            result = my_function(0)
            self.assertEqual(result, 0)
    
    
    if __name__ == '__main__':
        unittest.main()
    

    If I execute test method test_my_function() on my system it passes. The instruction sys.path.insert(1, '..') add a path where the test code search package.

    Output on my system

    If I change directory to the folder given_proj and execute the following command:

    > cd /path/to/given_proj
    
    /path/to/given_proj> python tests/test_main.py
    

    The output of the execution is the following:

    ['/path/to/given_proj/tests', '..', '/other/paths']
    .
    Ran 1 test in 0.000s
    
    OK
    

    In the output you can see the print of the content of sys.path.