## page was renamed from 10_Testing = Тестирование = (Дополнительно: [[LecturesCMC/PythonDevelopment2021/10_Testing_Mock|Оффлайн-лекция прошлого года про квазиобъекты]]) [[http://softwaretestingfundamentals.com|много теории без питона тут]] * Место тестирования в жизненном цикле программного продукта * Собирается — значит, работает! * + Проверяется анализатором кода, и потыкано end-user тестерами согласно тест-плану * + Автоматическая проверка работоспособности там, где это возможно * + Измерение тестового покрытия (coverage) * Ручное, автоматизированное, автоматическое * [[https://ru.wikipedia.org/wiki/Тестирование_программного_обеспечения|Виды тестирования]] * [[http://softwaretestingfundamentals.com/software-testing-levels|Уровни]] * unit — спецификация компонентов (функций/классов и т. п.) * integration — спецификация интерфейсов ''между'' компонентами * system — спецификация конечного продукта * acceptance — потребительские/рыночные/эксплуатационные/… свойства продукта * Тест регрессий * Покрытие * Функции / классы * Строчки кода * Различные execution path (?) * Различные execution path с критически различными наборами данных (??) * Test-driven development: * Сначала весь код, потом некоторые тесты (когда это можно?) * Каждая новая фича сопровождается тестом * [[RW:Разработка_через_тестирование]] (TDD) 1. сначала пишется тест и заглушка 1. сам код ''падает'' (иначе бесполезен) 1. под тест пишется код 1. код не падает 1. код изменяется и ''всё равно'' не падает * green test trap: Тестирование может доказать наличие дефектов, но не их отсутствие * red test trap: Не всякие проваленные тесты означают дефекты. Могут означать пробел в требованиях, в том числе нефункциональных * Полезные ≠ друг другу термины: * '''ошибка''' программиста при написании программы может привести к * '''дефекту''' (багу) в программе, который в свою очередь может * проявиться (или не проявиться) в виде программного '''сбоя''' * Стоимость исправления дефекта возрастает пропорционально его «возрасту» * Непрерывная интеграция == Модульное тестирование в Python == === Doctest === [[py3doc:doctest]]: тест = диалог с python-интерпретатором ==== Пример использования doctest ==== * [[LecturesCMC/PythonDevelopment2020/09_Testing#Doctest|В лекциях прошлого года]] * [[https://git.sr.ht/~frbrgeorge/MooTest/log|В репозитории с примерами к этой лекции]] Поэтому только кратко: 1.#0 Модуль 1. Тестируем вручную из командной строки 1. Добавляем диалог as is в docstring: 1. Тестирование: `python3 -m doctest Moo.py` 1. Отчёт (с успешными тестами): `python3 -m doctest -v Moo.py` 1. Тестирование исключений * Вообще говоря, важны только три строчки (сама команда, первая строка и сообщение с исключением), остальные можно выкинуть 1. [[py3doc:doctest#simple-usage-checking-examples-in-a-text-file|Перенос тестов во внешний текстовый файл]], * например, в `.rst` * этот файл отлично включается в Sphinx-документацию * Запуск `python3 exttest.py -v`: {{{#!highlight python import doctest doctest.testfile("exttest.rst") }}} === «Серьёзные» фреймворки === Как правило — реализация методологии [[RW:xUnit]] Fixture:: Подготовка компонента к тесту: не все функции можно оттестировать сходу, иногда надо 1. сначала что-то создать, открыть, запустить, … (''set-up'') 1. провести тест 1. удалить, закрыть, остановить, … это что-то (''tear-down'') Такое ''одно'' что-то называет fixture Case:: ''Что именно'' тестируем. Подготавливаем окружение (fixtur-ы), что-то дёргаем, смотрим, подходит ли результат SubCase:: Множественное тестирование (например, тестирование в цикле обработки нескольких наборов параметров) Suite:: Набор cases (на разные темы, разных больших частей, разных уровней и т. п.) Runner:: Запускалка тестов, обработчик отчётов и т. п. === Модуль unittest === Принципы [[py3doc:unittest]]: * Тестирующая функция вызывает проверочный метод `assertЧтоТоТам(какие-то, параметры)` или проверяет в контекстном менеджере, что выпало нужное исключение / warning * Case — это класс, в нём несколько атомарных тестирующих функций * Suite — это объект, в который можно добавлять Case и другие Suite * Runner — заводится автоматом, но можно задать вручную с выбором Suite * Fixture — два метода в Case (`.SetUp()` и `.TearDown()`) Возможности: * Сбор тестов (discovery) * Пропуск тестов по условию * Ожидаемый сбой * Подтесты * Обработка сигналов ==== Примеры использования ==== * [[https://git.sr.ht/~frbrgeorge/MooTest/log|В репозитории примеров к лекции]] * [[https://tirinox.ru/unit-test-python/|В методичке]] === Квазиобъекты (mock) === Основные понятия: Квазиобъект (mock pbject, mocker):: объект, создаваемый в процессе тестирования ''вместо'' «настоящего» объекта * Как правило, умеет всё сразу (его можно вызывать с любыми параметрами, обращаться к любым полям внутри него и т. п.) * Умеет отчитываться (такой-то метод был вызван так-то) * Свойства объекта и его полей (которые создаются автоматически как такие же квазиобъекты) — настраиваемые. Например, можно задать возвращаемые значения, значение некоторых полей, вызываемые исключения и т. п. Патч (patch или monkey patch):: подмена на время теста полей реального объекта на квазиобъекты * Нередко обладает свойством самоудаляться по окончании теста. Например, патч оформляется как [[py3doc:stdtypes.html#typecontextmanager|контекстный менеджер]] Индикатор (sentinel):: уникальный объект, который передаётся в тестируемую подсистему, и по окончании теста должен продолжать существовать где-то в её недрах * Если индикатор в процессе тестирования удаляется, тест не пройден [[py3doc:unittest.mock]] * [[py3doc:unittest.mock-examples.html|примеры]] * [[LecturesCMC/PythonDevelopment2021/10_Testing_Mock|Зачем нужны квазиобъекты]], статья на эту тему * Небольшой пример использования квазиобъектов в [[https://git.sr.ht/~frbrgeorge/GradeProject2021|Модельном семестровом проекте]] * (для понимания стоит открыть исходный код проекта) {{{#!python class TestDateTime(unittest.TestCase): date_init = "%c" def setUp(self): self.view = MagicMock() self.view.sFormat.get = MagicMock(return_value=self.date_init) self.view.sStart.get = MagicMock(return_value="1") self.view.sCaltype.get = MagicMock(return_value=3) self.view.Date, self.view.Calendar = {}, {} self.model = AppModel(self.view) self.control = AppControl(self.model) def test_0_init(self): assert self.model.view is self.view assert self.control.model is self.model def test_1_call(self): self.model(self.control) self.view.assert_called_once_with(self.control) self.view.sFormat.get.assert_called_once() self.view.sStart.get.assert_called_once() self.view.sCaltype.get.assert_called_once() self.assertEqual(self.view.Date["text"], time.strftime(self.date_init)) res = subprocess.run(["cal", "-3"], capture_output=True) 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()` === Тестовое покрытие === (''[[https://pypi.org/search/?q=coverage|тысячи их]]'') Модуль [[pypi:coverage]] * поддержка unittest, pytest (и слегка старого уже nose) * выборочное покрытие * маркировка ветвлений * подпроцессы * журналирование контекста ==== Пример ==== [[https://git.sr.ht/~frbrgeorge/MooTest/log|В репозитории примеров к лекции]] * Набор тестов * Исключение того, что тестировать не надо === Модуль pytest === [[https://docs.pytest.org|pytest]] * Сравнение с `unittest`: * Test discover «из коробки» — поиск тестов по имени файла/функции/класаа (есть в `unittest`) * Обычный `assert` для теста (вместо многих функций) * Атомарные фикстуры (перенаправление в/в, временные изменения классов и т. п.) * Множество дополнений ==== Пример использования pytest: pudb ==== [[https://documen.tician.de/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` можно использовать отдельный модуль [[https://coverage.readthedocs.io|coverage]] == Д/З == * Осознать, что нуждается в unit-тестировании * Оснастить код семестрового проекта unit-тестами (любой фреймворк) * Зафиксировать в документации, как их запускать * Подумать над тестированием UI * например, с помощью порождения событий `tkinter`