Тестирование
TODO проредить: кажется, информации слишком много
(Дополнительно: Оффлайн-лекция прошлого года про квазиобъекты)
- Место тестирования в жизненном цикле программного продукта
- Собирается и устанавливается — значит, работает!
- + Проверяется анализатором кода, и потыкано end-user тестерами согласно тест-плану
- + Автоматическая проверка работоспособности там, где это возможно
- + Измерение тестового покрытия (coverage)
- + Покрытие всех execution path
- Ручное, автоматизированное, автоматическое
- unit — спецификация компонентов (функций/классов и т. п.)
integration — спецификация интерфейсов между компонентами
- system — спецификация конечного продукта
- acceptance — потребительские/рыночные/эксплуатационные/… свойства продукта
- Тест регрессий
- Покрытие
- Функции / классы
- Строчки кода
- Различные execution path (?)
- Различные execution path с критически различными наборами данных (??)
- Test-driven development:
- Альтернатива ленивому варианту «Сначала весь код, потом некоторые тесты» (когда это можно?)
- Каждая новая фича сопровождается тестом
Разработка_через_тестирование (TDD)
- сначала пишется тест и заглушка
сам код падает (иначе бесполезен)
- под тест пишется код
- код не падает
код изменяется и всё равно не падает
- green test trap: Тестирование может доказать наличие дефектов, но не их отсутствие
- red test trap: Не всякие проваленные тесты означают дефекты. Могут означать пробел в требованиях, в том числе нефункциональных
- Полезные ≠ друг другу термины:
ошибка программиста при написании программы может привести к
дефекту (багу) в программе, который в свою очередь может
проявиться (или не проявиться) в виде программного сбоя
- ⇒ Ошибки (особенно в ДНК) исправить почти невозможно, сбои исправлять почти бессмысленно, мы работаем именно с дефектами
- Стоимость исправления дефекта возрастает пропорционально его «возрасту»
- Непрерывная интеграция
Модульное тестирование в Python
Doctest
doctest: тест = диалог с python-интерпретатором
Пример использования doctest
Поэтому только кратко:
- Модуль
- Тестируем вручную из командной строки
- Добавляем диалог as is в docstring:
Тестирование: python3 -m doctest Moo.py
Отчёт (с успешными тестами): python3 -m doctest -v Moo.py
- Тестирование исключений
- Вообще говоря, важны только три строчки (сама команда, первая строка и сообщение с исключением), остальные можно выкинуть
Перенос тестов во внешний текстовый файл,
например, в .rst
- этот файл отлично включается в Sphinx-документацию
Запуск python3 exttest.py -v:
«Серьёзные» фреймворки
Как правило — реализация методологии xUnit
- Fixture
- Подготовка компонента к тесту: не все функции можно оттестировать сходу, иногда надо
сначала что-то создать, открыть, запустить, … (set-up)
- провести тест
удалить, закрыть, остановить, … это что-то (tear-down)
- Такое одно что-то называется fixture
- Test
Атомарная процедура тестирования. Как правило однократно сравнивает ожидаемый результат с полученным.
- Case
- Набор тестов для тестирования определённого свойства объекта. Подготавливаем окружение (fixtur-ы), изучаем заявленное свойство, в т. ч. в граничных условиях.
- SubCase
- Элемент множественного тестирования (например, при циклическом вызове теста на различных наборах данных)
- Suite
- Набор cases (на разные темы, разных больших частей, разных уровней, несовместимых с другим набором и т. п.)
- Runner
- Запускалка тестов, обработчик отчётов и т. п.
Модуль unittest
Принципы unittest:
Тестирующая функция вызывает проверочный метод assertЧтоТоТам(какие-то, параметры) или проверяет в контекстном менеджере, что выпало нужное исключение / warning
- Case — это класс, в нём несколько атомарных тестирующих функций
- Suite — это объект, в который можно добавлять Case и другие Suite
- Обычно возникает автоматически (тогда Suite — это модуль)
- Runner — это заводится автоматом, но можно задать вручную с выбором Suite
- Fixture:
В модуле: setUpModule() / tearDownModule() — один раз на Suite
В классе: .setUpClass() / .tearDownClass() — один раз на Case
В классе: .setUp…() / .tearDown…() — один раз на каждый тест
Возможности:
- Сбор тестов (discovery)
- Пропуск тестов по условию
- Ожидаемый сбой
- Подтесты
- Обработка сигналов
Примеры использования
Квазиобъекты (mock)
Основные понятия:
- Квазиобъект (mock pbject, mocker)
объект, создаваемый в процессе тестирования вместо «настоящего» объекта
- Как правило, умеет всё сразу (его можно вызывать с любыми параметрами, обращаться к любым полям внутри него и т. п.)
- Умеет отчитываться (такой-то метод был вызван так-то)
- Свойства объекта и его полей (которые создаются автоматически как такие же квазиобъекты) — настраиваемые. Например, можно задать возвращаемые значения, значение некоторых полей, вызываемые исключения и т. п.
- Патч (patch или monkey patch)
- подмена на время теста полей реального объекта на квазиобъекты
Нередко обладает свойством самоудаляться по окончании теста. Например, патч оформляется как контекстный менеджер
- Индикатор (sentinel)
- уникальный объект, который передаётся в тестируемую подсистему, и по окончании теста должен продолжать существовать где-то в её недрах
- Если индикатор в процессе тестирования удаляется, тест не пройден
Зачем нужны квазиобъекты, статья с примером на эту тему
Небольшой пример использования квазиобъектов в В репозитории примеров к лекции
Пример чуть побольше в Модельном семестровом проекте
- (для понимания стоит открыть исходный код проекта)
1 class TestDateTime(unittest.TestCase): 2 date_init = "%c" 3 4 def setUp(self): 5 self.view = MagicMock() 6 self.view.sFormat.get = MagicMock(return_value=self.date_init) 7 self.view.sStart.get = MagicMock(return_value="1") 8 self.view.sCaltype.get = MagicMock(return_value=3) 9 self.view.Date, self.view.Calendar = {}, {} 10 self.model = AppModel(self.view) 11 self.control = AppControl(self.model) 12 13 def test_0_init(self): 14 assert self.model.view is self.view 15 assert self.control.model is self.model 16 17 def test_1_call(self): 18 self.model(self.control) 19 self.view.assert_called_once_with(self.control) 20 self.view.sFormat.get.assert_called_once() 21 self.view.sStart.get.assert_called_once() 22 self.view.sCaltype.get.assert_called_once() 23 self.assertEqual(self.view.Date["text"], time.strftime(self.date_init)) 24 res = subprocess.run(["cal", "-3"], capture_output=True) 25 self.assertEqual(self.view.Calendar["text"], res.stdout.decode())
setUp() — подготовка фикстуры
- Вместо View используется квазиобъект (избавляемся от tkinter)
Вместо управляющих переменных — квазиобьекты с заданным поведением метода .gt()
Вместо «словарного» интерфейса по изменению настроек виджетов tkinter — настоящие словари, где будет оседать результат тестирования
- Остальные объекты настоящие
test_0_init() — примитивный тест
test_1_call() — тест активации всего комплекса Model→View→Control (с квази Vew)
- должен быть активирован View (с параметром Control)
- должны быть опрошены управляющие переменные (в процессе инициализации)
поля в View должны быть инициализированы результатами работы cal и strftime()
Тестовое покрытие
Модуль coverage
- поддержка unittest, pytest
- выборочное покрытие
- маркировка ветвлений
- подпроцессы
- журналирование контекста
Пример
В репозитории примеров к лекции
- Набор тестов
- Исключение того, что тестировать не надо
Модуль pytest
Сравнение с unittest:
Test discover «из коробки» — поиск тестов по имени файла/функции/класаа (есть в unittest)
Обычный assert для теста (вместо многих функций)
- Атомарные фикстуры (перенаправление в/в, временные изменения классов и т. п.)
- Множество дополнений
Пример использования pytest: pudb
PuDB — отладчик для питона
Соберём под него окружение:
[george@inspiron src]$ python3 -m venv init demo-pudb [george@inspiron src]$ cd demo-pudb [george@inspiron demo-pudb]$ . ./bin/activate (demo-pudb) [george@inspiron demo-pudb]$ git clone https://github.com/inducer/pudb.git Cloning into 'pudb'... cd pudb (demo-pudb) [george@inspiron pudb]$ ls debug_me.py example-theme.py pudb setup.py doc LICENSE README.rst test example-shell.py MANIFEST.in requirements.dev.txt try-the-debugger.sh example-stringifier.py manual-tests setup.cfg upload_coverage.sh (demo-pudb) [george@inspiron pudb]$
Cоберём модуль:
(demo-pudb) [george@inspiron pudb]$ pip install -r requirements.dev.txt Collecting codecov==2.0.5 (from -r requirements.dev.txt (line 1)) . . . Successfully installed Pygments-2.2.0 argparse-1.4.0 . . . (demo-pudb) [george@inspiron pudb]$ python setup.py build running build . . . copying pudb/ui_tools.py -> build/lib/pudb (demo-pudb) [george@inspiron pudb]$
Все нужные зависимости указаны в файла requirements.dev.txt
Собранный модуль в чистом виде лежит в build/lib/pudb
Для запуска тестов надо сделать так, чтобы build/lib попал в PYTHONPATH, тогда модуль будет экспортирован оттуда
(demo-pudb) [george@inspiron pudb]$ export PYTHONPATH=`pwd`/build/lib (demo-pudb) [george@inspiron pudb]$ pytest ========================= test session starts ========================= platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 rootdir: /home/george/src/demo-pudb/pudb, inifile: plugins: mock-1.10.0, cov-2.4.0 collected 16 items test/test_lowlevel.py .... test/test_make_canvas.py ..... test/test_settings.py .. test/test_source_code_providers.py .... test/test_var_view.py . ====================== 16 passed in 0.22 seconds ======================
pytest сам нашёл, где лежат тесты
Можно сказать pytest -v для отчёта по каждому тесту в файле
В этом проекте используется два дополнения к pytest:
- вместо полноценных фикстур — т. н. моккеры (mock):
(demo-pudb) [george@inspiron pudb]$ pytest --fixtures-per-test =========================== test session starts ============================ platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 rootdir: /home/george/src/demo-pudb/pudb, inifile: plugins: mock-1.10.0, cov-2.4.0 collected 16 items ------------------ fixtures used by test_load_breakpoints ------------------ ------------------------ (test/test_settings.py:10) ------------------------ mocker return an object that has the same interface to the `mock` module, but takes care of automatically undoing all patches after each test method. pytestconfig the pytest config object with access to command line opts. . . .
Замер покрытия кода тестами cov
(demo-pudb) [george@inspiron pudb]$ pytest --cov=build/lib/pudb =========================== test session starts ======================= platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 rootdir: /home/george/src/demo-pudb/pudb, inifile: plugins: mock-1.10.0, cov-2.4.0 collected 16 items test/test_lowlevel.py .... test/test_make_canvas.py ..... test/test_settings.py .. test/test_source_code_providers.py .... test/test_var_view.py . ----------- coverage: platform linux, python 3.8.2-final-0 ----------- Name Stmts Miss Branch BrPart Cover ----------------------------------------------------------------- build/lib/pudb/__init__.py 194 159 26 4 18% build/lib/pudb/__main__.py 3 3 0 0 0% build/lib/pudb/b.py 14 14 2 0 0% build/lib/pudb/debugger.py 1386 1276 223 2 7% build/lib/pudb/ipython.py 31 31 8 0 0% build/lib/pudb/lowlevel.py 134 61 56 7 51% build/lib/pudb/py3compat.py 23 10 2 1 56% build/lib/pudb/remote.py 120 120 12 0 0% build/lib/pudb/run.py 27 27 0 0 0% build/lib/pudb/settings.py 378 302 88 13 22% build/lib/pudb/shell.py 137 137 12 0 0% build/lib/pudb/source_view.py 230 167 36 7 26% build/lib/pudb/theme.py 595 595 2 0 0% build/lib/pudb/ui_tools.py 222 159 66 0 25% build/lib/pudb/var_view.py 396 324 92 6 18% ----------------------------------------------------------------- TOTAL 3890 3385 625 40 13% ======================== 16 passed in 0.55 seconds ===================
Без указания --cov=build/lib/pudb ключ --cov посчитает покрытие всего запускаемого кода на python (включая все системные библиотеки:)
Вместо дополнения к pytest можно использовать отдельный модуль coverage
Д/З
- Осознать, что нуждается в unit-тестировании
- Оснастить код семестрового проекта unit-тестами (любой фреймворк)
- Зафиксировать в документации, как их запускать
- Подумать над тестированием UI
например, с помощью порождения событий tkinter