Maybe my goal and what I try to do here is wrong in the meaning of unpythonic. I am open for any suggestions about that.
My goals
- Application (
myapp
) with its own tests folder. - A package (
mypackage
) with its own tests folder. - The tests for the package should be runable from the application folder and from the package folder.
- The package have implicit and explicit components. The latter need to be imported explicit (e.g. via
import mypackage.mymoduleB
). - The package (folder) can be copied (shipped for reuse in other applications?) to other file system locations without loosing its functionality and testabilibty. That is why
tests
is inside the package folder and not outside.
That is the folder tree where itest
is the name of the project, myapp
is the application with an if __name__ == '__main__':
in it and mypackag
is the package.
itest
└── myapp
├── myapp.py
├── mypackage
│ ├── __init__.py
│ ├── _mymoduleA.py
│ ├── mymoduleB.py
│ └── tests
│ ├── __init__.py
│ └── test_all.py
└── tests
├── __init__.py
└── test_myapp.py
The problem
I can run the unittests from the application directory without problems.
/home/user/tab-cloud/_transfer/itest/myapp $ python3 -m unittest -vvv
test_A (mypackage.tests.test_all.TestAll) ... mymoduleA.foo()
ok
test_B (mypackage.tests.test_all.TestAll) ... mymoduleB.bar()
ok
test_myname (tests.test_myapp.TestMyApp) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.001s
OK
But when I go inside the package the tests do not run (sieh goal #3).
/home/user/tab-cloud/_transfer/itest/myapp/mypackage $ python3 -m unittest -vvv
tests.test_all (unittest.loader._FailedTest) ... ERROR
======================================================================
ERROR: tests.test_all (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_all
Traceback (most recent call last):
File "/usr/lib/python3.9/unittest/loader.py", line 436, in _find_test_path
module = self._get_module_from_name(name)
File "/usr/lib/python3.9/unittest/loader.py", line 377, in _get_module_from_name
__import__(name)
File "/home/user/tab-cloud/_transfer/itest/myapp/mypackage/tests/test_all.py", line 12, in <module>
from . import mypackage
ImportError: cannot import name 'mypackage' from 'tests' (/home/user/tab-cloud/_transfer/itest/myapp/mypackage/tests/__init__.py)
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)
The MWE
No I show you the files. To make sure the tests for the package using the right import
when run from the application folder or from the package folder I use importlib
(based on foreign solution).
The three files form the package
This is myapp/mypackage/__init__.py
:
# imported implicite via 'mypackage'
from ._mymoduleA import *
# 'mymoduleB' need to be imported explicite
# via 'mypackage.moduleB'
This is myapp/mypackage/_mymoduleA.py
:
def foo():
print('mymoduleA.foo()')
return 1
This is myapp/mypackage/mymoduleB.py
:
def bar():
print('mymoduleB.bar()')
return 2
The tests for the package
The myapp/mypackage/tests/__init__.py
is empty.
This is myapp/mypackage/tests/test_all.py
:
import importlib
import unittest
# The package should be able to be tested by itself (run unittest inside the
# package directory) AND from the using application (run unittest in
# application directory).
# Based on: https://stackoverflow.com/a/14050282/4865723
if importlib.util.find_spec('mypackage'):
import mypackage
import mypackage.mymoduleB
else:
from . import mypackage
from mypackage import mymoduleB
class TestAll(unittest.TestCase):
def test_A(self):
self.assertEqual(1, mypackage.foo())
def test_B(self):
self.assertEqual(2, mypackage.mymoduleB.bar())
The application
This is cat myapp/myapp.py
:
#!/usr/bin/env python3
import mypackage
def myname():
return 'My application!'
if __name__ == '__main__':
print(myname())
mypackage.foo()
try:
mypackage.mymoduleB.bar()
except AttributeError:
# we expecting this
print('Not imported yet: "mymoduleB.bar()"')
# this should work
import mypackage.mymoduleB
mypackage.mymoduleB.bar()
The test for the application
The myapp/tests/__init__.py
is empty.
This is myapp/tests/test_myapp.py
:
import unittest
import myapp
class TestMyApp(unittest.TestCase):
def test_myname(self):
self.assertEqual(myapp.myname(), 'My application!')
Sidenotes
Please let me explain something more about my goals. The mypackage
should be reusable in other projects. In practice this means I copy the mypackage
folder from one place to another. And while copy that folder I do want that tests
folder come with it without explicte thinking about it because it is outside the package folder. And if the new project does unittesting the tests of the package should be involved in that unittesting automaticlly (via discover
).