В Django есть соглашение относительно расположения тестов в приложениях. По нему в tests.py живут тесты, работающие с unittest. Однажды я задумался о размещении тестов в отдельных файлах, то есть о преобразовании модуля tests в пакет tests.
Почитал django/test/simple.py. Получилось, что если в модуле tests определяется callable переменная suite, то значение, возвращенное suite, добавляется к уже обнаруженным тестам. Если такой переменной не обнаружено, то из модуля tests пытаются загрузить тесты стандартным лоадером unittest.defaultTestLoader.loadTestsFromModule.
Спросил в
django-russian.
Предложенное решение немного не устроило - в каждый новый пакет tests кладётся адаптированный test_all.py, из которого импортируется suite.
Мне нужно было, чтобы:
- решение позволяло вызывать тесты как по manage.py test app, так и по manage.py test app.TestCase,
- основной код решения лежал бы где-то в утилитах проекта, и в __init__.py было бы просто пара строчек.
Попробовал переработать решение esizikov с функцией suite. Получилось не очень хорошо.
import os.path
import glob
import unittest
def suite():
suite = unittest.TestSuite()
file_full_path = os.path.abspath(os.path.join(__file__, '..'))
app_name = file_full_path.split("/")[-2]
for filename in glob.glob(file_full_path + "/[a-z]*.py"):
module_name = os.path.basename(filename).split(".py")[0]
full_module_name = "%s.tests.%s" % (app_name, module_name)
suite.addTest(
unittest.defaultTestLoader.loadTestsFromModule(
__import__(full_module_name, {}, {}, [full_module_name])
)
)
return suite
Работает manage.py test app, не работает manage.py test app.TestCase.
Попробовал просто импортировать всё из модулей пакета.
from autologin_realtor import *
from login_homeowner import *
Хорошо, всё работает. Но вписывать каждый новый модуль беспокойно. Пробуем импортировать тест-кейсы из модулей пакета автоматически.
import os
import glob
import unittest
import inspect
fr = inspect.currentframe()
file_full_path = os.path.abspath(os.path.join(__file__, '..'))
app_name = file_full_path.split("/")[-2]
for filename in glob.glob(file_full_path + "/[a-z]*.py"):
module_name = os.path.basename(filename).split(".py")[0]
full_module_name = "%s.tests.%s" % (app_name, module_name)
m = __import__(full_module_name, {}, {}, [module_name])
mdir = dir(m)
for name in mdir:
attr = getattr(m, name)
if type(attr) is type:
if issubclass(attr, unittest.TestCase):
fr.f_globals[name] = attr
Немного о том, что происходит. Импортируем модули, имена которых начинаются с латинской буквы, во избежание рекурсивного импорта __init__.py. Проверяем имена, видимые в каждом модуле. Если под каким-то именем оказывается класс-потомок unittest.TestCase, то создаём в нашем модуле (то есть в пакете tests) переменную с таким именем, указывающую на этот самый класс-потомок. То есть фактически импортируем руками нужные классы в пакет.
Хорошо, всё работает. Но это кусок кода лежит прямо в __init__.py. Развяжем немного.
Оставим в __init__.py только импорт функции и её вызов. Определение функции и необходимые для её работы импорты переносим в project.utils. Попутно изменяем функцию: теперь используется f_globals не текущего фрейма, а предыдущего.
project/app/tests/__init__.py:
from project.utils import theimport
theimport(__file__)
project/utils.py:
import os
import glob
import unittest
import inspect
def theimport(module_path):
fr = inspect.stack()[1][0]
file_full_path = os.path.abspath(os.path.join(module_path, '..'))
app_name = file_full_path.split("/")[-2]
for filename in glob.glob(file_full_path + "/[a-z]*.py"):
module_name = os.path.basename(filename).split(".py")[0]
full_module_name = "%s.tests.%s" % (app_name, module_name)
m = __import__(full_module_name, {}, {}, [module_name])
mdir = dir(m)
for name in mdir:
attr = getattr(m, name)
if type(attr) is type:
if issubclass(attr, unittest.TestCase):
fr.f_globals[name] = attr