python
April 23, 2023

Как запускать тесты для нескольких модулей с unittest

Программы без ошибок можно написать двумя способами, но работает- третий (Алан Перлис). В больших проектах тесты разбиваются на несколько модулей. Для их одновременного запуска в библиотеке unittest существуют специальные инструменты:

  • TestSuite - класс для агрегирования тестов из нескольких модулей;
  • TestLoader - класс, умеющий находить тесты по заданным путям и возврата объекта TestSuite;
  • TextTestRunner - класс для запуска тестов из TestSuite.

Пусть у нас есть 2 подпапки с тестами в папке lists:

Алгоритм настройки запуска тестов такой:

  • Во всех папках, где есть тесты создайте init.py, так как TestLoader находит их в импортируемых модулях;
  • Создайте экземпляр класса TestLoader и вызовите метод discover с указанием корневой директории проекта, откуда должен быть доступен импорт модуля (top_level_dir), а также директории поиска (start_dir) и шаблона (pattern);
  • сформируйте из возвращаемых значений итоговый объект TestSuite;
  • создайте TextTestRunner и запустите метод run с объектом TestSuite в качестве параметра.

Напомню, что сами модульные тесты создаются как методы в классе, наследуемом TestCase (читать подробнее).

Создадим TestLoader, который находит тесты по шаблону в подпапках папки lists:

import unittest

dn = 'lists'
loader = unittest.TestLoader()
suite = loader.discover(start_dir=dn, pattern=r'test_web_ops*', top_level_dir='.')

suite

Количество найденных тестов можно получить методом countTestCases:

suite.countTestCases()

Так suite запускается с TextTestRunner:

runner = unittest.TextTestRunner()
runner.run(suite)

Для задания более сложных условий отбора файлов для тестирования вас будет ограничивать синтаксис аргумента pattern в методе discover, поддерживающим только unix шаблоны. Поэтому, как вариант, можно отобрать список модулей другим способом, а затем для каждого вызвать discover, добавляя возвращаемый им suite методом addTest к объекту suite, аккумулирующему результат. Например, соберем в список модули, содержащие слово web:

from itertools import chain

fn_l = list(chain(*[[it for it in os.listdir(dn+'/'+subdn) \
                    if not (('web' in it) or it.startswith('.') or it.startswith('__'))]\
                   for subdn in os.listdir(dn) if not '.' in subdn and 'test' in subdn]))
fn_l

Затем с discover возвратим TestSuite для каждого модуля и добавим в итоговый:

suite = unittest.TestSuite()
for fn in fn_l:
    suite.addTest(loader.discover(start_dir=dn, pattern=fn, top_level_dir='.'))

suite.countTestCases()

Аналогично запустим runner:

runner.run(suite)