Тестирование
- Место тестирования в жизненном цикле программного продукта 
- Собирается — значит, работает!
 - + Проверяется анализатором кода, и потыкано end-user тестерами согласно тест-плану
 - + Автоматическая проверка работоспособности там, где это возможно
 - + Измерение тестового покрытия (coverage)
 
 - Ручное, автоматизированное, автоматическое
 - unit — спецификация компонентов (функций/классов и т. п.)
 integration — спецификация интерфейсов между компонентами
- system — спецификация конечного продукта
 - acceptacne — потребительские/рыночные/эксплутационные/… свойства продукта
 
- Дисциплина: 
- Сначала весь код, потом некоторые тесты (когда это можно?)
 - Каждая новая фича сопровождается тестом
 Разработка_через_тестирование (TDD)
- сначала пишется тест и заглушка
 сам код падает (иначе бесполезен)
- под тест пишется код
 - код не падает
 код изменяется и всё равно не падает
 - green test trap: Тестирование может доказать наличие дефектов, но не их отсутствие
 - red test trap: Не всякие проваленные тесты означают дефекты. Могут означать пробел в требованиях, в том числе нефункциональных
 - Полезные ≠ друг другу термины: 
ошибка программиста при написании программы может привести к
дефекту (багу) в программе, который в свою очередь может
проявиться (или не проявиться) в виде программного сбоя
 - Стоимость исправления дефекта возрастает пропорционально его «возрасту»
 - Непрерывная интеграция
 
Модульное тестирование в Python
Doctest
doctest: тест = диалог с python-интерпретатором
- Модуль
 - Тестируем вручную:
 - Добавляем тесты в docstring: 
def moo(oos=2, end=""): '''Издать мычание длиной oos с end в конце Оба параметра необязательны: >>> moo() 'Moo' Первый задаёт количество букв 'o' в слове 'Moo' >>> moo(4) 'Mooooo' Букв 'o' может и не быть >>> moo(0) 'M' Второй задаёт символ после всех 'o' (по умолчанию — ничего) >>> moo(end='!') 'Moo!' >>> moo(0,'?') 'M?' ''' return "M"+"o"*oos+end - Тестирование: 
1 $ python3 -m doctest Moo.py 2 ********************************************************************** 3 File "/home/george/src/moo/Moo.py", line 13, in Moo.moo 4 Failed example: 5 moo(4) 6 Expected: 7 'Mooooo' 8 Got: 9 'Moooo' 10 ********************************************************************** 11 1 items had failures: 12 1 of 5 in Moo.moo 13 ***Test Failed*** 1 failures. 14
 - Отчёт (с успешными тестами): 
1 $ python3 -m doctest -v Moo.py 2 Trying: 3 moo() 4 Expecting: 5 'Moo' 6 ok 7 Trying: 8 moo(4) 9 Expecting: 10 'Mooooo' 11 ********************************************************************** 12 File "/home/george/src/moo/Moo.py", line 13, in Moo.moo 13 Failed example: 14 moo(4) 15 Expected: 16 'Mooooo' 17 Got: 18 'Moooo' 19 Trying: 20 moo(0) 21 Expecting: 22 'M' 23 ok 24 Trying: 25 moo(end='!') 26 Expecting: 27 'Moo!' 28 ok 29 Trying: 30 moo(0,'?') 31 Expecting: 32 'M?' 33 ok 34 1 items had no tests: 35 Moo 36 ********************************************************************** 37 1 items had failures: 38 1 of 5 in Moo.moo 39 5 tests in 2 items. 40 4 passed and 1 failed. 41 ***Test Failed*** 1 failures. 42
 
Тестирование исключений:
Как обычно, добавим просто весь вывод!
   1 def moo(oos=2, end=""):
   2     '''Издать мычание длиной oos с end в конце
   3 ...
   4 
   5 Здесь должно быть исключение:
   6 >>> moo("QQ")
   7 Traceback (most recent call last):
   8   File "<stdin>", line 1, in <module>
   9   File "/home/george/src/tests/Moo.py", line 33, in moo
  10     return "M"+"o"*moos+end
  11 TypeError: can't multiply sequence by non-int of type 'str'
  12 '''
Вообще говоря, важны только три строчки, остальные можно выкинуть
...
Здесь должно быть исключение:
>>> moo("QQ")
Traceback (most recent call last):
TypeError: can't multiply sequence by non-int of type 'str'
...Тесты должны пройти!
Перенос тестов во внешний файл
Пишем файл ( 
 можно в .rst, для Sphinx), например, exttest.rst: 
К нему запускалку тестов:
И запускаем её (ключи как у модуля pytest):
$ python3 exttest.py -v
Trying:
    import Moo
Expecting nothing
ok
Trying:
    Moo.moo(5)
Expecting:
    'Mooooo'
ok
1 items passed all tests:
   2 tests in exttest.rst
2 tests in 1 items.
2 passed and 0 failed.
Test passed.
«Серьёзные» фреймворки
- Fixture
 - Подготовка компонента к тесту: не все функции можно оттестировать сходу, иногда надо  
сначала что-то создать, открыть, запустить, … (set-up)
- провести тест
 удалить, закрыть, остановить, … это что-то (tear-down) Такое одно что-то называет fixture
 - Case
 Что именно тестируем. Подготавливаем окружение (fixtur-ы), что-то дёргаем, смотрим, подходит ли результат
- Suite
 - Набор cases (на разные темы, разных больших частей, разных уровней и т. п.)
 - Runner
 - Запускалка тестов, обработчик отчётов и т. п.
 
Сравнение с unittest:
Test discover «из коробки» — поиск тестов по имени файла/функции/класаа (есть в unittest)
Обычный assert для теста (вместо многих функций)
- Атомарные фикстуры (пеернаправление в/в, временные изменения классов и т. п.)
 - Множество дополнений
 
Ещё тестеры:
Пример: pudb
PuDB — отладчик для питона
Соберём под него окружение:
   1 [george@inspiron src]$ python3 -m venv init demo-pudb
   2 [george@inspiron src]$ cd demo-pudb
   3 [george@inspiron demo-pudb]$ . ./bin/activate
   4 (demo-pudb) [george@inspiron demo-pudb]$ git clone https://github.com/inducer/pudb.git
   5 Cloning into 'pudb'...
   6 cd pudb
   7 (demo-pudb) [george@inspiron pudb]$  ls
   8 debug_me.py             example-theme.py  pudb                  setup.py
   9 doc                     LICENSE           README.rst            test
  10 example-shell.py        MANIFEST.in       requirements.dev.txt  try-the-debugger.sh
  11 example-stringifier.py  manual-tests      setup.cfg             upload_coverage.sh
  12 (demo-pudb) [george@inspiron pudb]$
  13 
Cоберём модуль:
   1 (demo-pudb) [george@inspiron pudb]$ pip install -r requirements.dev.txt 
   2 Collecting codecov==2.0.5 (from -r requirements.dev.txt (line 1))
   3 . . .
   4 Successfully installed Pygments-2.2.0 argparse-1.4.0 . . .
   5 (demo-pudb) [george@inspiron pudb]$ python setup.py build
   6 running build
   7 . . .
   8 copying pudb/ui_tools.py -> build/lib/pudb
   9 (demo-pudb) [george@inspiron pudb]$
  10 
Все нужные зависимости указаны в файла requirements.dev.txt
Собранный модуль в чистом виде лежит в build/lib/pudb
Для запуска тестов надо сделать так, чтобы build/lib попал в PYTHONPATH, тогда модуль будет экспортирован оттуда
   1 (demo-pudb) [george@inspiron pudb]$ export PYTHONPATH=`pwd`/build/lib
   2 (demo-pudb) [george@inspiron pudb]$ pytest                             
   3 ========================= test session starts =========================
   4 platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
   5 rootdir: /home/george/src/demo-pudb/pudb, inifile:
   6 plugins: mock-1.10.0, cov-2.4.0
   7 collected 16 items 
   8 
   9 test/test_lowlevel.py ....
  10 test/test_make_canvas.py .....
  11 test/test_settings.py ..
  12 test/test_source_code_providers.py ....
  13 test/test_var_view.py .
  14 
  15 ====================== 16 passed in 0.22 seconds ======================
  16 
pytest сам нашёл, где лежат тесты
Можно сказать pytest -v для отчёта по каждому тесту в файле
В этом проекте используется два дополнения к pytest:
- вместо полноценных фикстур — т. н. моккеры (mock): 
1 (demo-pudb) [george@inspiron pudb]$ pytest --fixtures-per-test 2 =========================== test session starts ============================ 3 platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 4 rootdir: /home/george/src/demo-pudb/pudb, inifile: 5 plugins: mock-1.10.0, cov-2.4.0 6 collected 16 items 7 8 ------------------ fixtures used by test_load_breakpoints ------------------ 9 ------------------------ (test/test_settings.py:10) ------------------------ 10 mocker 11 return an object that has the same interface to the `mock` module, but 12 takes care of automatically undoing all patches after each test method. 13 pytestconfig 14 the pytest config object with access to command line opts. 15 . . . 16
 Замер покрытия кода тестами cov
1 (demo-pudb) [george@inspiron pudb]$ pytest --cov=build/lib/pudb 2 =========================== test session starts ======================= 3 platform linux -- Python 3.8.2, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 4 rootdir: /home/george/src/demo-pudb/pudb, inifile: 5 plugins: mock-1.10.0, cov-2.4.0 6 collected 16 items 7 8 test/test_lowlevel.py .... 9 test/test_make_canvas.py ..... 10 test/test_settings.py .. 11 test/test_source_code_providers.py .... 12 test/test_var_view.py . 13 14 ----------- coverage: platform linux, python 3.8.2-final-0 ----------- 15 Name Stmts Miss Branch BrPart Cover 16 ----------------------------------------------------------------- 17 build/lib/pudb/__init__.py 194 159 26 4 18% 18 build/lib/pudb/__main__.py 3 3 0 0 0% 19 build/lib/pudb/b.py 14 14 2 0 0% 20 build/lib/pudb/debugger.py 1386 1276 223 2 7% 21 build/lib/pudb/ipython.py 31 31 8 0 0% 22 build/lib/pudb/lowlevel.py 134 61 56 7 51% 23 build/lib/pudb/py3compat.py 23 10 2 1 56% 24 build/lib/pudb/remote.py 120 120 12 0 0% 25 build/lib/pudb/run.py 27 27 0 0 0% 26 build/lib/pudb/settings.py 378 302 88 13 22% 27 build/lib/pudb/shell.py 137 137 12 0 0% 28 build/lib/pudb/source_view.py 230 167 36 7 26% 29 build/lib/pudb/theme.py 595 595 2 0 0% 30 build/lib/pudb/ui_tools.py 222 159 66 0 25% 31 build/lib/pudb/var_view.py 396 324 92 6 18% 32 ----------------------------------------------------------------- 33 TOTAL 3890 3385 625 40 13% 34 ======================== 16 passed in 0.55 seconds =================== 35
Без указания --cov=build/lib/pudb ключ --cov посчитает покрытие всего запускаемого кода на python (включая все системные библиотеки:)
Вместо дополнения к pytest можно использовать отдельный модуль coverage
Д/З
- Осознать, что нуждается в unit-тестировании
 - Оснастить код семестрового проекта unit-тестами (любой фреймворк) 
- Зафиксировать в документации, как их запускать
 
 - Подумать над тестированием UI 
например, с помощью порождения событий tkinter
 
